依赖名称
在 模板(类模板和函数模板)的定义内部,某些构造的含义可能因实例化而异。特别是,类型和表达式可能取决于类型模板参数的类型和非类型模板参数的值。
template<typename T> struct X : B<T> // “B<T>” is dependent on T { typename T::A* pa; // “T::A” is dependent on T // (see below for the meaning of this use of “typename”) void f(B<T>* pb) { static int i = B<T>::i; // “B<T>::i” is dependent on T pb->j++; // “pb->j” is dependent on T } };
依赖名称和非依赖名称的名称查找和绑定是不同的。
内容 |
[编辑] 绑定规则
非依赖名称在模板定义点被查找和绑定。即使在模板实例化点存在更好的匹配,此绑定也成立
如果非依赖名称的含义在模板的定义上下文和特化的实例化点之间发生更改,则程序是非良构的,不需要诊断。这在以下情况下是可能发生的
- 非依赖名称中使用的类型在定义点是不完整的,但在实例化点是完整的
|
(自 C++17 起) |
- 实例化使用在定义点未定义的默认实参或默认模板实参
- 实例化点的常量表达式使用整数或无作用域枚举类型的 const 对象的值、constexpr 对象的值、引用的值或 constexpr 函数的定义(自 C++11 起),并且该对象/引用/函数(自 C++11 起)在定义点未定义
- 模板在实例化点使用非依赖类模板特化或变量模板特化(自 C++14 起),并且它使用的此模板要么是从定义点未定义的偏特化实例化的,要么是命名了在定义点未声明的显式特化
依赖名称的绑定被推迟到查找发生时。
[编辑] 查找规则
模板中使用的依赖名称的查找被推迟到模板实参已知时,此时
- 非 ADL 查找检查从模板定义上下文可见的具有外部链接的函数声明
- ADL 检查从模板定义上下文或模板实例化上下文可见的具有外部链接的函数声明
(换句话说,在模板定义后添加新的函数声明不会使其可见,除非通过 ADL)。
此规则的目的是帮助防止违反模板实例化的 ODR
// an external library namespace E { template<typename T> void writeObject(const T& t) { std::cout << "Value = " << t << '\n'; } } // translation unit 1: // Programmer 1 wants to allow E::writeObject to work with vector<int> namespace P1 { std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) { for (int n : v) os << n << ' '; return os; } void doSomething() { std::vector<int> v; E::writeObject(v); // Error: will not find P1::operator<< } } // translation unit 2: // Programmer 2 wants to allow E::writeObject to work with vector<int> namespace P2 { std::ostream& operator<<(std::ostream& os, const std::vector<int>& v) { for (int n : v) os << n << ':'; return os << "[]"; } void doSomethingElse() { std::vector<int> v; E::writeObject(v); // Error: will not find P2::operator<< } }
在上面的示例中,如果允许从实例化上下文进行 operator<<
的非 ADL 查找,则 E::writeObject<vector<int>> 的实例化将具有两个不同的定义:一个使用 P1::operator<<,另一个使用 P2::operator<<。这种 ODR 违规可能不会被链接器检测到,从而导致两者之一在两个实例中使用。
为了使 ADL 检查用户定义的命名空间,应将 std::vector 替换为用户定义的类,或者其元素类型应为用户定义的类
namespace P1 { // if C is a class defined in the P1 namespace std::ostream& operator<<(std::ostream& os, const std::vector<C>& v) { for (C n : v) os << n; return os; } void doSomething() { std::vector<C> v; E::writeObject(v); // OK: instantiates writeObject(std::vector<P1::C>) // which finds P1::operator<< via ADL } }
注意:此规则使得为标准库类型重载运算符变得不切实际
#include <iostream> #include <iterator> #include <utility> #include <vector> // Bad idea: operator in global namespace, but its arguments are in std:: std::ostream& operator<<(std::ostream& os, std::pair<int, double> p) { return os << p.first << ',' << p.second; } int main() { typedef std::pair<int, double> elem_t; std::vector<elem_t> v(10); std::cout << v[0] << '\n'; // OK, ordinary lookup finds ::operator<< std::copy(v.begin(), v.end(), std::ostream_iterator<elem_t>(std::cout, " ")); // Error: both ordinary lookup from the point of definition of // std::ostream_iterator and ADL will only consider the std namespace, // and will find many overloads of std::operator<<, so the lookup will be done. // Overload resolution will then fail to find operator<< for elem_t // in the set found by the lookup. }
注意:依赖名称的有限查找(但不是绑定)也在模板定义时发生,这是区分它们与非依赖名称以及确定它们是当前实例化的成员还是未知特化的成员所必需的。通过此查找获得的信息可用于检测错误,请参见下文。
[编辑] 依赖类型
以下类型是依赖类型
- 模板形参
- 未知特化的成员(见下文)
- 嵌套类/枚举,它是未知特化的依赖成员(见下文)
- 依赖类型的 cv 限定版本
- 从依赖类型构造的复合类型
- 数组类型,其元素类型是依赖的,或者其边界(如果有)是值依赖的
|
(自 C++11 起) |
- 函数类型,其异常规范是值依赖的
- 模板 ID,其中
- 模板名称是模板形参,或者
- 任何模板实参都是类型依赖的,或是值依赖的,或是包扩展(自 C++11 起)(即使模板 ID 在没有其实参列表的情况下使用,如注入类名称中)
应用于类型依赖表达式的 decltype 的结果是唯一的依赖类型。只有当它们的表达式等效时,两个这样的结果才引用相同的类型。 |
(自 C++11 起) |
应用于类型依赖常量表达式的包索引说明符是唯一的依赖类型。只有当它们的常量表达式等效时,两个这样的包索引说明符才引用相同的类型。否则,只有当它们的索引具有相同的值时,两个这样的包索引说明符才引用相同的类型。 |
(自 C++26 起) |
注意:当前实例化的 typedef 成员仅在其引用的类型是依赖类型时才是依赖的。
[编辑] 类型依赖表达式
以下表达式是类型依赖的
- 包含标识符,其名称查找找到至少一个依赖声明
- 包含依赖模板 ID
|
(自 C++11 起) |
|
(自 C++14 起) |
|
(自 C++17 起) |
|
(自 C++26 起) |
- 任何转换为依赖类型的转型表达式
- new 表达式,用于创建依赖类型的对象
- 成员访问表达式,引用当前实例化的成员,其类型是依赖的
- 成员访问表达式,引用未知特化的成员
(自 C++17 起) |
|
(自 C++26 起) |
以下表达式永远不是类型依赖的,因为这些表达式的类型不可能是
(自 C++11 起) |
(自 C++20 起) |
[编辑] 值依赖表达式
以下表达式是值依赖的
|
(自 C++20 起) |
- 它是类型依赖的。
- 它是非类型模板形参的名称。
- 它命名静态数据成员,它是当前实例化的依赖成员,并且未初始化。
- 它命名静态成员函数,它是当前实例化的依赖成员。
- 它是具有整数或枚举(直到 C++11)字面量(自 C++11 起)类型的常量,从值依赖表达式初始化。
- 以下表达式,其中操作数是类型依赖表达式
(自 C++11 起) |
- 以下表达式,其中操作数是依赖类型 ID
- 以下表达式,其中目标类型是依赖的,或者操作数是类型依赖表达式
- 函数风格转型表达式,其中目标类型是依赖的,或者值依赖表达式用括号括起来或大括号(自 C++11 起)
(自 C++11 起) | |
(自 C++17 起) |
- 取地址表达式,其中实参是限定标识符,它命名当前实例化的依赖成员
- 取地址表达式,其中实参是任何表达式,作为核心常量表达式求值时,引用模板实体,它是具有静态或线程存储(自 C++11 起)持续时间的对象或成员函数。
[编辑] 依赖名称
本节不完整 原因:缺少 [temp.dep] 中的引言(标识符表达式后跟带括号的列表... |
本节不完整 原因:重新措辞以使其更清晰(或至少不那么令人生畏),并在执行此操作时应用 CWG 问题 591 |
[编辑] 当前实例化
在类模板定义(包括其成员函数和嵌套类)中,某些名称可能会被推断为引用当前实例化。这允许在定义点而不是实例化点检测到某些错误,并消除了对依赖名称的 typename 和 template 消歧义器的要求,请参见下文。
只有以下名称可以引用当前实例化
- 在类模板、类模板的嵌套类、类模板的成员或类模板的嵌套类的成员的定义中
- 类模板或嵌套类的注入类名称
- 在主类模板或主类模板的成员的定义中
- 类模板的名称后跟主模板的模板实参列表(或等效的别名模板特化),其中每个实参都与其对应的形参等效(如下定义)。
- 在类模板的嵌套类的定义中
- 嵌套类的名称用作当前实例化的成员
- 在类模板偏特化或类模板偏特化的成员的定义中
- 类模板的名称后跟偏特化的模板实参列表,其中每个实参都与其对应的形参等效
- 在模板函数的定义中
- 局部类的名称
如果满足以下条件,则模板实参与模板形参等效
- 它具有与模板形参相同的类型(忽略 cv 限定),并且
- 其初始化器由单个标识符组成,该标识符命名模板形参,或者递归地,命名这样的变量。
template<class T> class A { A* p1; // A is the current instantiation A<T>* p2; // A<T> is the current instantiation ::A<T>* p4; // ::A<T> is the current instantiation A<T*> p3; // A<T*> is not the current instantiation class B { B* p1; // B is the current instantiation A<T>::B* p2; // A<T>::B is the current instantiation typename A<T*>::B* p3; // A<T*>::B is not the current instantiation }; }; template<class T> class A<T*> { A<T*>* p1; // A<T*> is the current instantiation A<T>* p2; // A<T> is not the current instantiation }; template<int I> struct B { static const int my_I = I; static const int my_I2 = I + 0; static const int my_I3 = my_I; static const long my_I4 = I; static const int my_I5 = (I); B<my_I>* b1; // B<my_I> is the current instantiation: // my_I has the same type as I, // and it is initialized with only I B<my_I2>* b2; // B<my_I2> is not the current instantiation: // I + 0 is not a single identifier B<my_I3>* b3; // B<my_I3> is the current instantiation: // my_I3 has the same type as I, // and it is initialized with only my_I (which is equivalent to I) B<my_I4>* b4; // B<my_I4> is not the current instantiation: // the type of my_I4 (long) is not the same as the type of I (int) B<my_I5>* b5; // B<my_I5> is not the current instantiation: // (I) is not a single identifier };
请注意,如果嵌套类从其封闭类模板派生,则基类可以是当前实例化。作为依赖类型但不是当前实例化的基类是依赖基类
template<class T> struct A { typedef int M; struct B { typedef void M; struct C; }; }; template<class T> struct A<T>::B::C : A<T> { M m; // OK, A<T>::M };
如果一个名称是以下之一,则该名称被归类为当前实例化的成员
- 在当前实例化或其非依赖基类中通过非限定查找找到的非限定名称。
- 限定名称,如果限定符(
::
左侧的名称)命名当前实例化,并且在当前实例化或其非依赖基类中查找找到该名称 - 在类成员访问表达式(y 在 x.y 或 xp->y 中)中使用的名称,其中对象表达式(x 或 *xp)是当前实例化,并且在当前实例化或其非依赖基类中查找找到该名称
template<class T> class A { static const int i = 5; int n1[i]; // i refers to a member of the current instantiation int n2[A::i]; // A::i refers to a member of the current instantiation int n3[A<T>::i]; // A<T>::i refers to a member of the current instantiation int f(); }; template<class T> int A<T>::f() { return i; // i refers to a member of the current instantiation }
当前实例化的成员可以是依赖的,也可以是非依赖的。
如果当前实例化的成员的查找在实例化点和定义点之间给出不同的结果,则查找是不明确的。但是请注意,当使用成员名称时,它不会自动转换为类成员访问表达式,只有显式成员访问表达式才指示当前实例化的成员
struct A { int m; }; struct B { int m; }; template<typename T> struct C : A, T { int f() { return this->m; } // finds A::m in the template definition context int g() { return m; } // finds A::m in the template definition context }; template int C<B>::f(); // error: finds both A::m and B::m template int C<B>::g(); // OK: transformation to class member access syntax // does not occur in the template definition context
[编辑] 未知特化
在模板定义中,某些名称被推断为属于未知特化,特别是,
- 限定名称,如果
::
左侧出现的任何名称是依赖类型,但不是当前实例化的成员 - 限定名称,其限定符是当前实例化,并且在当前实例化或其任何非依赖基类中未找到该名称,并且存在依赖基类
- 类成员访问表达式中成员的名称(y 在 x.y 或 xp->y 中),如果对象表达式(x 或 *xp)的类型是依赖类型,并且不是当前实例化
- 类成员访问表达式中成员的名称(x.y 或 xp->y 中的 y),如果对象表达式(x 或 *xp)的类型是当前实例化,且该名称在当前实例化或其任何非依赖基类中都找不到,并且存在依赖基类
template<typename T> struct Base {}; template<typename T> struct Derived : Base<T> { void f() { // Derived<T> refers to current instantiation // there is no “unknown_type” in the current instantiation // but there is a dependent base (Base<T>) // Therefore, “unknown_type” is a member of unknown specialization typename Derived<T>::unknown_type z; } }; template<> struct Base<int> // this specialization provides it { typedef int unknown_type; };
此分类允许在模板定义点(而不是实例化点)检测到以下错误
- 如果任何模板定义具有限定名称,其中限定符引用当前实例化,并且该名称既不是当前实例化的成员,也不是未知特化的成员,则程序是非良构的(无需诊断),即使模板从未实例化。
template<class T> class A { typedef int type; void f() { A<T>::type i; // OK: “type” is a member of the current instantiation typename A<T>::other j; // Error: // “other” is not a member of the current instantiation // and it is not a member of an unknown specialization // because A<T> (which names the current instantiation), // has no dependent bases for “other” to hide in. } };
- 如果任何模板定义具有成员访问表达式,其中对象表达式是当前实例化,但该名称既不是当前实例化的成员,也不是未知特化的成员,则程序是非良构的,即使模板从未实例化。
未知特化的成员始终是依赖的,并且在实例化点像所有依赖名称一样被查找和绑定(见上文)
[编辑] 依赖名称的 typename 消歧义符
在模板(包括别名模板)的声明或定义中,如果名称不是当前实例化的成员,并且依赖于模板参数,则除非使用关键字 typename,或者除非它已被确立为类型名称(例如,通过 typedef 声明或用作基类的名称),否则该名称不被视为类型。
#include <iostream> #include <vector> int p = 1; template<typename T> void foo(const std::vector<T> &v) { // std::vector<T>::const_iterator is a dependent name, typename std::vector<T>::const_iterator it = v.begin(); // without “typename”, the following is parsed as multiplication // of the type-dependent data member “const_iterator” // and some variable “p”. Since there is a global “p” visible // at this point, this template definition compiles. std::vector<T>::const_iterator* p; typedef typename std::vector<T>::const_iterator iter_t; iter_t * p2; // “iter_t” is a dependent name, but it is known to be a type name } template<typename T> struct S { typedef int value_t; // member of current instantiation void f() { S<T>::value_t n{}; // S<T> is dependent, but “typename” not needed std::cout << n << '\n'; } }; int main() { std::vector<int> v; foo(v); // template instantiation fails: there is no member variable // called “const_iterator” in the type std::vector<int> S<int>().f(); }
关键字 typename 只能以这种方式在限定名称(例如 T::x)之前使用,但名称不必是依赖的。
通常的限定名称查找用于以 typename 为前缀的标识符。与详述类型说明符的情况不同,尽管有限定符,但查找规则不会更改
struct A // A has a nested variable X and a nested type struct X { struct X {}; int X; }; struct B { struct X {}; // B has a nested type struct X }; template<class T> void f(T t) { typename T::X x; } void foo() { A a; B b; f(b); // OK: instantiates f<B>, T::X refers to B::X f(a); // error: cannot instantiate f<A>: // because qualified name lookup for A::X finds the data member }
关键字 typename 甚至可以在模板外部使用。
#include <vector> int main() { // Both OK (after resolving CWG 382) typedef typename std::vector<int>::const_iterator iter_t; typename std::vector<int> v; }
在某些上下文中,只有类型名称可以有效地出现。在这些上下文中,依赖的限定名称被假定为命名类型,并且不需要 typename
|
(自 C++20 起) |
[编辑] 依赖名称的 template 消歧义符
类似地,在模板定义中,如果依赖名称不是当前实例化的成员,则除非使用消歧义关键字 template,或者除非它已被确立为模板名称,否则不被视为模板名称
template<typename T> struct S { template<typename U> void foo() {} }; template<typename T> void bar() { S<T> s; s.foo<T>(); // error: < parsed as less than operator s.template foo<T>(); // OK }
关键字 template 只能以这种方式在运算符 ::(作用域解析)、->(通过指针的成员访问)和 .(成员访问)之后使用,以下都是有效的示例
- T::template foo<X>();
- s.template foo<X>();
- this->template foo<X>();
- typename T::template iterator<int>::value_type v;
与 typename 的情况一样,即使名称不是依赖的,或者使用未出现在模板的作用域中,也允许使用 template 前缀。
即使 :: 左侧的名称引用命名空间,也允许使用 template 消歧义符
template<typename> struct S {}; ::template S<void> q; // allowed, but unnecessary
由于成员访问表达式中模板名称的非限定名称查找的特殊规则,当非依赖模板名称出现在成员访问表达式中(在 -> 或 . 之后)时,如果存在与表达式上下文中普通查找找到的名称相同的类或别名(自 C++11 起)模板,则消歧义符是不必要的。但是,如果表达式上下文中查找找到的模板与类上下文中找到的模板不同,则程序是非良构的(直到 C++11) template<int> struct A { int value; }; template<class T> void f(T t) { t.A<0>::value; // Ordinary lookup of A finds a class template. // A<0>::value names member of class A<0> // t.A < 0; // Error: “<” is treated as the start of template argument list } |
(直到 C++23) |
[编辑] 关键字
[编辑] 缺陷报告
以下行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
DR | 应用于 | 已发布行为 | 正确行为 |
---|---|---|---|
CWG 206 | C++98 | 在模板定义时使用非依赖名称的类型不完整时,何时应用语义约束是未指定的 在模板定义时使用非依赖名称的类型不完整时,何时应用语义约束是未指定的 在模板定义点类型不完整,但在 实例化执行点类型完整 |
程序是非良构的 且无需诊断 在这种情况下需要 |
CWG 224 | C++98 | 依赖类型的定义基于 名称的形式而不是查找 |
定义已修改 |
CWG 382 | C++98 | typename 消歧义符仅允许在模板作用域中使用 | 也允许在 模板外部使用 |
CWG 468 | C++98 | template 消歧义符仅允许在模板作用域中使用 | 也允许在 模板外部使用 |
CWG 502 | C++98 | 嵌套枚举是否依赖是未指定的 | 作为嵌套类依赖 |
CWG 1047 | C++98 | typeid 表达式从不是值依赖的 | 如果 操作数是类型依赖的,则为值依赖 |
CWG 1160 | C++98 | 当与主模板或部分匹配的模板 ID 当与主模板或部分匹配的模板 ID 特化出现在模板成员的定义中时,名称是否引用当前实例化是未指定的 |
已指定 |
CWG 1413 | C++98 | 未初始化的静态数据成员、静态成员函数和地址 类模板成员的未被列为值依赖 |
已列出 |
CWG 1471 | C++98 | 当前实例化的非依赖基类的嵌套类型 是依赖的 |
它不是依赖的 |
CWG 1850 | C++98 | 意义可能在 定义上下文和实例化点之间更改的情况列表是不完整的 |
已完成 |
CWG 1929 | C++98 | 是否 template 消歧义符可以 跟在 :: 后面,且 :: 左侧的名称引用命名空间,这一点不明确 |
允许 |
CWG 2066 | C++98 | this 从不是值依赖的 | 它可能是 值依赖的 |
CWG 2100 | C++98 | 类的静态数据成员的地址 模板未被列为值依赖 |
已列出 |
CWG 2109 | C++98 | 类型依赖的标识符表达式可能不是值依赖的 | 它们总是 值依赖的 |
CWG 2276 | C++98 | 异常规范是值依赖的函数类型 不是依赖类型 |
它是 |
CWG 2307 | C++98 | 用作 模板参数的带括号的非类型模板参数等同于该模板参数 |
不再等同 |
CWG 2457 | C++11 | 具有函数参数的函数类型 包不是依赖类型 |
它是 |
CWG 2785 | C++20 | requires 表达式可能是类型依赖的 | 它们从不 是类型依赖的 |
CWG 2905 | C++11 | noexcept 表达式仅是值依赖的 如果其操作数是值依赖的 |
它是值依赖的 如果其操作数涉及 模板参数 |
CWG 2936 | C++98 | 模板化函数的局部类的名称 不属于当前实例化的一部分 |
它们是 |