Dependent names
在 模板 (包括 类模板 和 函数模板 )的定义内部,某些结构的含义可能因实例化不同而有所差异。具体而言,类型和表达式可能依赖于类型模板参数的类型以及常量模板参数的值。
template<typename T> struct X : B<T> // “B<T>” 依赖于 T { typename T::A* pa; // “T::A” 依赖于 T // (关于此处 “typename” 用法的含义请参见下文) void f(B<T>* pb) { static int i = B<T>::i; // “B<T>::i” 依赖于 T pb->j++; // “pb->j” 依赖于 T } };
名称 查找 与绑定对于依赖名称和非依赖名称的处理方式有所不同。
目录 |
绑定规则
非依赖名称在模板定义点进行查找和绑定。即使模板实例化点存在更优匹配,该绑定仍然有效:
如果非依赖名称的含义在模板的定义上下文与其特化的实例化点之间发生变化,则该程序是非良构的,且不要求诊断。以下情况可能出现这种问题:
- 在非依赖名称中使用的类型,在定义点处是 不完整类型 ,但在实例化点处变为完整类型
|
(since C++17) |
- 实例化使用了在定义点尚未定义的默认实参或默认模板实参
- 实例化点的 常量表达式 使用了以下值:整型或无作用域枚举类型的 const 对象值 、constexpr 对象值、引用值或 constexpr 函数的定义 (C++11 起) ,且该 对象/引用/函数 (C++11 起) 在定义点尚未定义
- 模板在实例化点使用了非依赖类模板特化 或变量模板特化 (C++14 起) ,且该模板要么实例化自定义点尚未定义的偏特化,要么指名了定义点尚未声明的显式特化
依赖名称的绑定将推迟至查找发生时进行。
查找规则
在模板中使用的 查找 依赖名称的操作会推迟到模板参数已知时才进行,此时
- 非ADL查找会检查从模板定义上下文中可见的具有外部链接的函数声明
- ADL 会检查从模板定义上下文或模板实例化上下文中可见的具有外部链接的函数声明
(换句话说,在模板定义后添加新的函数声明不会使其可见,除非通过 ADL)。
本规则旨在帮助防范模板实例化违反 ODR 的情况:
// 外部库 namespace E { template<typename T> void writeObject(const T& t) { std::cout << "Value = " << t << '\n'; } } // 翻译单元 1: // 程序员 1 希望让 E::writeObject 能够处理 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); // 错误:无法找到 P1::operator<< } } // 翻译单元 2: // 程序员 2 希望让 E::writeObject 能够处理 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); // 错误:无法找到 P2::operator<< } }
在上述示例中,如果允许从实例化上下文进行对
operator<<
的非ADL查找,那么
E
::
writeObject
<
vector
<
int
>>
的实例化将存在两种不同的定义:一种使用
P1
::
operator
<<
,另一种使用
P2
::
operator
<<
。这样的ODR(单一定义规则)违反可能不会被链接器检测到,导致在两个实例中都使用其中某一个定义。
为使ADL检查用户定义命名空间,需将 std::vector 替换为用户定义类,或其元素类型应为用户定义类:
namespace P1 { // 若 C 是定义在 P1 命名空间中的类 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); // 正确:实例化 writeObject(std::vector<P1::C>) // 通过 ADL 查找到 P1::operator<< } }
注意:此规则使得为标准库类型重载运算符变得不切实际:
#include <iostream> #include <iterator> #include <utility> #include <vector> // 不良实践:运算符位于全局命名空间,但其参数位于 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'; // 正常:普通查找会找到 ::operator<< std::copy(v.begin(), v.end(), std::ostream_iterator<elem_t>(std::cout, " ")); // 错误:从 std::ostream_iterator 定义点进行的普通查找和 ADL 查找 // 都只会考虑 std 命名空间,并将找到多个 std::operator<< 重载,因此查找会完成。 // 随后重载解析将无法在查找找到的集合中找到适用于 elem_t 的 operator<< }
注意:在模板定义时也会对依赖名称进行有限查找(但不具有约束力),这是为了区分它们与非依赖名称,同时也为了确定它们是当前实例化的成员还是未知特化的成员。通过此查找获得的信息可用于检测错误,详见下文。
依赖类型
以下类型属于 依赖类型 :
- 模板参数
- 未知特化的成员(见下文)
- 作为未知特化的依赖成员的嵌套类/枚举(见下文)
- 依赖类型的cv限定版本
- 由依赖类型构造的复合类型
- 元素类型依赖或边界(若存在)为值依赖的数组类型
|
(since C++11) |
- 其异常规范具有值依赖性的函数类型
- 满足以下条件的 模板标识 :
-
- 模板名称是模板参数,或
- 任何模板实参是类型依赖的,或值依赖的 ,或是包展开 (C++11 起) (即使该模板标识在没有其实参列表的情况下使用,如 注入类名 )
应用于类型依赖表达式的 decltype 的结果是一个唯一的依赖类型。仅当它们的表达式 等价 时,两个这样的结果才会引用同一类型。 |
(since C++11) |
应用于类型依赖常量表达式的包索引说明符是一个独特的依赖类型。两个这样的包索引说明符仅在它们的常量表达式等价时引用同一类型。否则,两个这样的包索引说明符仅在它们的索引具有相同值时引用同一类型。 |
(since C++26) |
注意:当前实例化的 typedef 成员仅当其所指类型为依赖类型时才具有依赖性。
类型依赖表达式
以下表达式是 类型依赖 的:
-
- 包含一个标识符,其名称查找找到至少一个依赖声明
- 包含一个依赖的 template-id
|
(since C++11) |
|
(since C++14) |
|
(since C++17) |
|
(since C++26) |
- 任何转型表达式到依赖类型
- new expression 创建依赖类型对象
- 引用当前实例化中类型依赖的成员的成员访问表达式
- 引用未知特化的成员的成员访问表达式
| (C++17 起) |
|
(C++26 起) |
以下表达式永远不会是类型依赖的,因为这些表达式的类型不能是:
| (自 C++11 起) |
| (自 C++20 起) |
值依赖表达式
以下表达式是 值依赖的 :
|
(since C++20) |
-
- 它是类型相关的。
- 它是常量模板参数的名称。
- 它命名一个属于当前实例化的依赖成员且未初始化的静态数据成员。
- 它命名一个属于当前实例化的依赖成员的静态成员函数。
- 它是一个具有 整型或枚举 (C++11 前) 字面量 (C++11 起) 类型的常量,由值相关表达式初始化。
- 以下表达式,其中操作数是类型依赖表达式:
| (自 C++11 起) |
- 以下表达式,其中操作数是依赖类型标识符:
- 以下表达式,其中目标类型是依赖类型或操作数是类型依赖表达式:
- 函数式转换 表达式,其中目标类型是依赖类型或被括号 或花括号 (C++11 起) 包围的值依赖表达式
| (C++11 起) | |
| (C++17 起) |
- 取址表达式,其中实参是命名当前实例化依赖成员的 限定标识符
- 取址表达式,其中实参是作为核心 常量表达式 求值时,引用具有静态 或线程存储期 (C++11 起) 的对象的 模板化实体 ,或是成员函数的任意表达式
依赖名
|
本节内容不完整
原因:[temp.dep]章节的引导部分缺失(标识符表达式后接括号列表... |
|
本节内容不完整
原因:需重新措辞以提升表述清晰度(或至少降低理解难度),同时需应用 CWG issue 591 |
当前实例化
在类模板定义中(包括其成员函数和嵌套类),某些名称可被推导为指向 当前实例化 。这使得某些错误能在定义点而非实例化时被检测到,同时消除了对依赖名称使用 typename 和 template 消歧符的要求,详见下文。
只有以下名称可以指代当前实例化:
-
在类模板定义中、类模板的嵌套类定义中、类模板成员定义中,或类模板嵌套类成员定义中:
- 该类模板或嵌套类的注入类名
-
在主类模板定义中或主类模板成员定义中:
- 后随模板实参列表的类模板名称(或等价的别名模板特化),其中每个实参与其对应形参等价(定义见下文)
-
在类模板的嵌套类定义中:
- 作为当前实例化成员使用的嵌套类名称
-
在类模板偏特化定义中或类模板偏特化成员定义中:
- 后随偏特化模板实参列表的类模板名称,其中每个实参与其对应形参等价
-
在
模板化函数
定义中:
- 局部类 的名称
模板实参与模板形参等价的条件是
-
- 其类型与模板参数相同(忽略 cv 限定符),且
- 其初始化器由单个标识符组成,该标识符命名模板参数或递归地命名此类变量。
template<class T> class A { A* p1; // A 是当前实例化 A<T>* p2; // A<T> 是当前实例化 ::A<T>* p4; // ::A<T> 是当前实例化 A<T*> p3; // A<T*> 不是当前实例化 class B { B* p1; // B 是当前实例化 A<T>::B* p2; // A<T>::B 是当前实例化 typename A<T*>::B* p3; // A<T*>::B 不是当前实例化 }; }; template<class T> class A<T*> { A<T*>* p1; // A<T*> 是当前实例化 A<T>* p2; // A<T> 不是当前实例化 }; 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> 是当前实例化: // my_I 与 I 类型相同, // 且仅使用 I 初始化 B<my_I2>* b2; // B<my_I2> 不是当前实例化: // I + 0 不是单一标识符 B<my_I3>* b3; // B<my_I3> 是当前实例化: // my_I3 与 I 类型相同, // 且仅使用 my_I(等价于 I)初始化 B<my_I4>* b4; // B<my_I4> 不是当前实例化: // my_I4 的类型(long)与 I 的类型(int)不同 B<my_I5>* b5; // B<my_I5> 不是当前实例化: // (I) 不是单一标识符 };
请注意,如果嵌套类从其外围类模板派生,则基类可以是当前实例化。属于依赖类型但不是当前实例化的基类称为 依赖基类 :
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; // 正确,A<T>::M };
若某个名称属于当前实例化的成员,当它
- 通过 非限定查找 在当前实例化或其非依赖基类中找到的非限定名称
-
限定名称
,若限定符(
::左侧的名称)指代当前实例化,且查找在当前实例化或其非依赖基类中找到该名称 - 类成员访问表达式( x. y 或 xp - > y )中使用的名称,其中对象表达式( x 或 * xp )为当前实例化,且查找在当前实例化或其非依赖基类中找到该名称
template<class T> class A { static const int i = 5; int n1[i]; // i 引用当前实例化的成员 int n2[A::i]; // A::i 引用当前实例化的成员 int n3[A<T>::i]; // A<T>::i 引用当前实例化的成员 int f(); }; template<class T> int A<T>::f() { return i; // i 引用当前实例化的成员 }
当前实例化的成员可以是依赖的也可以是非依赖的。
如果在实例化点与定义点对当前实例化成员的查找给出不同结果,则该查找是歧义的。但请注意,当使用成员名称时,它不会自动转换为类成员访问表达式,只有显式的成员访问表达式才表示当前实例化的成员:
struct A { int m; }; struct B { int m; }; template<typename T> struct C : A, T { int f() { return this->m; } // 在模板定义上下文中查找 A::m int g() { return m; } // 在模板定义上下文中查找 A::m }; template int C<B>::f(); // 错误:同时找到 A::m 和 B::m template int C<B>::g(); // 正确:类成员访问语法的转换 // 不会在模板定义上下文中发生
未知特化
在模板定义中,某些名称会被推导为属于 未知特化 ,具体而言,
-
限定名称,如果出现在
::左侧的任何名称是一个依赖类型且不是当前实例化的成员 - 限定名称,其限定符为当前实例化,且在当前实例化或其任何非依赖基类中未找到该名称,同时存在依赖基类
- 类成员访问表达式中的成员名称( x. y 或 xp - > y 中的 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> 指代当前实例化 // 当前实例化中不存在 "unknown_type" // 但存在依赖基类 (Base<T>) // 因此 "unknown_type" 属于未知特化的成员 typename Derived<T>::unknown_type z; } }; template<> struct Base<int> // 此特化提供了该类型 { typedef int unknown_type; };
该分类允许在模板定义点(而非实例化点)检测以下错误:
- 如果任何模板定义中出现 限定名称查找 ,其中限定符指向当前实例化,且该名称既非当前实例化的成员也非未知特化的成员,则程序非良构(不要求诊断),即使该模板从未被实例化。
template<class T> class A { typedef int type; void f() { A<T>::type i; // 正确:“type”是当前实例化的成员 typename A<T>::other j; // 错误: // “other”不是当前实例化的成员 // 也不是未知特化的成员 // 因为 A<T>(指代当前实例化) // 没有可供“other”隐藏的依赖基类 } };
- 如果任何模板定义包含成员访问表达式,其中对象表达式是当前实例化,但该名称既不是当前实例化的成员也不是未知特化的成员,则程序是非良构的,即使该模板从未被实例化。
未知特化的成员始终是依赖性的,并在实例化点进行查找和绑定,如同所有依赖名称(见上文)
用于依赖名的 typename 消歧符
在模板(包括别名模板)的声明或定义中,对于不属于当前实例化成员且依赖于模板形参的名称,除非使用关键字 typename ,或该名称已通过类型别名声明等方式被确认为类型名(例如用于命名基类),否则不会被视作类型。
#include <iostream> #include <vector> int p = 1; template<typename T> void foo(const std::vector<T> &v) { // std::vector<T>::const_iterator 是一个依赖名称, typename std::vector<T>::const_iterator it = v.begin(); // 若无 "typename",下列代码将被解析为类型依赖的数据成员 "const_iterator" // 与某变量 "p" 的乘法运算。由于此时存在全局可见的 "p",该模板定义可通过编译 std::vector<T>::const_iterator* p; typedef typename std::vector<T>::const_iterator iter_t; iter_t * p2; // "iter_t" 是依赖名称,但已知其为类型名称 } template<typename T> struct S { typedef int value_t; // 当前实例化的成员 void f() { S<T>::value_t n{}; // S<T> 是依赖名称,但无需使用 "typename" std::cout << n << '\n'; } }; int main() { std::vector<int> v; foo(v); // 模板实例化失败:类型 std::vector<int> 中不存在名为 "const_iterator" 的成员变量 S<int>().f(); }
关键字 typename 仅可在限定名称前以此方式使用(例如 T :: x ),但该名称不必是依赖名称。
通常的 限定名称查找 规则适用于由 typename 前缀修饰的标识符。与 详细类型说明符 的情况不同,尽管存在限定符,查找规则并不会改变:
struct A // A 拥有嵌套变量 X 和嵌套类型 struct X { struct X {}; int X; }; struct B { struct X {}; // B 拥有嵌套类型 struct X }; template<class T> void f(T t) { typename T::X x; } void foo() { A a; B b; f(b); // 正确:实例化 f<B>,T::X 指向 B::X f(a); // 错误:无法实例化 f<A>: // 因为对 A::X 的限定名称查找找到的是数据成员 }
关键字 typename 即使在模板外部也可以使用。
#include <vector> int main() { // 两者均正确(在解决 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>(); // 错误:< 被解析为小于运算符 s.template foo<T>(); // 正确 }
关键字 template 仅可在运算符 :: (作用域解析)、 - > (通过指针访问成员)和 . (成员访问)之后以这种方式使用,以下均为有效示例:
- T :: template foo < X > ( ) ;
- s. template foo < X > ( ) ;
- this - > template foo < X > ( ) ;
- typename T :: template iterator < int > :: value_type v ;
标签内的C++代码均未翻译,仅对列表项外的说明文字进行了简体中文翻译。由于原文中除代码外无其他需翻译的文本内容,故仅添加本说明)
与 typename 的情况类似,即使名称不依赖模板参数或使用未出现在模板作用域内,也允许使用 template 前缀。
即使
::
左侧的名称指向命名空间,模板消歧符仍被允许:
template<typename> struct S {}; ::template S<void> q; // 允许但非必需
|
由于成员访问表达式中模板名称的 非限定名称查找 特殊规则,当非依赖模板名称出现在成员访问表达式( - > 之后或 . 之后)时,若通过表达式上下文的普通查找能找到同名的 类模板或别名模板 (C++11 起) ,则消歧义符并非必需。 但若通过表达式上下文查找找到的模板与通过类上下文找到的模板不同,则程序非良构 (C++11 前) template<int> struct A { int value; }; template<class T> void f(T t) { t.A<0>::value; // A 的普通查找找到类模板 // A<0>::value 表示类 A<0> 的成员 // t.A < 0; // 错误:“<”被视作模板实参列表的起始符 } |
(C++23 前) |
关键词
缺陷报告
下列行为变更缺陷报告被追溯应用于先前发布的 C++ 标准。
| 缺陷报告 | 应用于 | 发布时行为 | 正确行为 |
|---|---|---|---|
| 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出现在模板成员定义中时,
未规定名称是否引用当前实例化 |
已明确规定 |
| 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 | 模板化函数的局部类名称不属于当前实例化 | 现属于当前实例化 |