在封闭类模板中引用专用化时成员类模板的隐式实例化


#include <iostream>
template<typename T>
struct A{
template<typename U>
struct B{};
B<T> b;   //#2
};
//#3
int main() {
A<int> a;  // #1
}

考虑上面的代码,使用 template-idA<int>会导致专用化A<int>的隐式实例化。根据以下规则:

温度点#4

对于类模板专用化、类成员模板专用化或类模板的类成员的专用化,如果专用化是隐式实例化的,因为它是从另一个模板专用化中引用的,如果引用专用

化的上下文依赖于模板参数,并且如果在封闭模板实例化之前未实例化专用化, 实例化点紧接在封闭模板的实例化点之前否则,此类专用化的实例化点紧接在引用专用化的命名空间范围声明或定义之前。

对于#1中引用的专用化,该规则的"否则"部分将适用于它。这意味着,A<int>的实例化点应该在#3,这里没有问题。但是,A<int>的实例化将导致作为其成员的专用化B<int>的实例化。因此,该规则的if部分将适用于B<int>。我感到困惑的是这里。根据相关规则,B<int>的实例化点应在封闭模板的实例化点之前,即#3之前的某个地方,我在这里无法理解。如果通过normal class思考,只有两种方法可以定义其类成员,一种是在类定义中定义类成员,另一种是在类定义中声明类成员,然后在类定义之后的某个点定义类定义之外的类成员。

使用普通类更改示例:

struct Normal_A_Int{
struct Normal_B_Int{};
Normal_B_Int b;
};
int main(){
Normal_A_Int a;
}

这意味着,成员类Normal_B_Int的定义必须在封闭类的定义中,因为声明Normal_B_Int b需要一个完整的类类型。

那么,如何让成员类的定义放在封闭类B<int>A<int>的定义之前的某个点呢?充其量,B<int>的定义应该在A<int>的定义中。如何解释第一个例子的 POI?

您从 temp.point/4 提供的报价正是这种情况:

template<typename T>
struct A {
template<typename U>
struct B { };
B<T> b;
};
// point-of-instantiation for A<int>::B<int>
// point-of-instantiation for A<int>
int main() {
A<int> a;
}

没有太多的律师工作要做。标准说这些是实例化点。类模板不是类,直觉不一定会延续。它们既有定义又有实例化。

以外部类定义为例。如果是类,则必须定义成员的类型。如果是类模板,则只能声明成员的类型。您可以降低 B 的定义:

template<typename T>
struct A {
template<typename U> struct B;
B<T> b;
};
template<typename T>
template<typename U>
struct A<T>::B { };

您无法对类执行此操作(定义中有一个"不完整"成员),但可以使用类模板执行此操作。

那么问题来了,为什么模板的实例化点A<T>::B<T>A<T>的实例化点之前?最终,因为标准是这样说的。但请考虑一下,如果是之后,则根本无法拥有内部类模板。如果它,比如说,在A<T>的定义中,名称查找将行为不端,因为A定义和A<int>实例化点之间的名称在A<int>::B<int>中不可见。所以这实际上是有道理的。

也许直觉来自将定义和实例化点混为一谈:

那么,如何让成员类 B 的定义放在封闭类 A 的定义之前的某个点呢?

其实不然。实例化点控制名称可见性。TU 中该点之前的所有名称都可见。(这不是定义。从这个角度来看,直觉上很明显,A<int>::B<int>应该有一个接近A<int>实例化点的实例化点(它们应该看到相同的其他名称)。如果有排序,则内部可能应该排在第一位(以便A<int>可以有一个A<int>::B<int>成员)。如果没有排序,则必须有关于实例化如何交错或交互的语言。

这条规则有两个有趣的方面。

一是模板可以专业化。因此,要满足此要求,当编译器去实例化A<int>时,它必须首先选择专用化,对其进行足够的处理,以知道它有一个成员类模板并且需要实例化它,然后停止所有内容以首先实例化A<int>::B<int>。这并不困难,但很微妙。可以说有一个实例化堆栈。

第二个方面要有趣得多。您可能会期望从普通的类直觉中期望B<T>的定义可以使用模板上下文中需要实例化A<T>A<T>的东西(例如typedefs),而A<T>::B<T>实例化时尚不存在。喜欢:

template<typename T>
struct A {
using type = T;
template<typename U>
struct B { using type = typename A<U>::type; };
B<T> b;
};

可以吗?

如果A<int>::B<int>的实例化点先于A<int>的实例化点,我们就不能真正形成A<int>::type

这是CWG287和P1787的领域。

CWG287建议实例化点相同(一个不在另一个之前)。此外,它还将增加:

如果隐式实例化的类模板专用化、类成员专用化或类模板的专用化引用了类、类模板专用化、类成员专用化或包含直接或间接导致实例化的专用化引用的类模板的专用化,那么类引用的

完整性和顺序要求将应用于专用化引用的上下文中。

在我的示例中,A<int>::B<int>引用A<int>直接导致其实例化的方式通过引用B<int>,因此类引用(typename A<int>::type)的完整性和顺序要求应用于专用引用(B<int> b)的上下文中。所以没关系。如果 typedef 低于B的定义,仍然可以。但是如果我们将 typedef 移动到成员b下方,它将是格式错误的!微妙!这具有交错实例化的效果。当我们看到成员时,我们停止我们正在做的事情,去实例化A<int>::B<int>,但我们可以使用我们在实例化A<int>时使用排序和完整性要求。实例化点是相同的,因此我们也可以使用来自 TU 的相同声明。

CWG287似乎旨在复制编译器已经做的事情。然而,CWG287自2001年以来一直开放。(另请参阅此和此。

P1787似乎以C++23为目标,旨在重写许多微妙的语言。我认为旨在达到与CWG287类似的效果。但要做到这一点,他们必须彻底地重新定义名称查找,以至于我很难知道这一点。:)

好吧,我确实在您的第一个示例中运行了 CppInsights: https://cppinsights.io/lnk?code=dGVtcGxhdGU8dHlwZW5hbWUgVD4Kc3RydWN0IEF7CiAgICB0ZW1wbGF0ZTx0eXBlbmFtZSBVPgogICAgc3RydWN0IEJ7fTsKICAgIEI8VD4gYjsKfTsKCmludCBtYWluKCkgewogICAgQTxpbnQ+IGE7Cn0KCgoK&insightsOptions=cpp2a&std=cpp2a&rev=1.0

最新更新