目录
1. 什么是模板的依赖名?
在模板的定义中(包含类模板和函数模板),有些结构的意义可能因实例的不同而不同,特别是,类型和表达式可能依赖于类型模板参数的类型和非类型模板参数的值。
template<typename T>
struct X : B<T> // “B<T>” 依赖于 T (类型模板参数T )
{
typename T::A* pa; // “T::A” 依赖于 T (如果不使用typename,则编译器为认为
// 上式是乘法表达式 )
// (参见“typename”的这种用法之意义)
void f(B<T>* pb)
{
static int i = B<T>::i; // “B<T>::i” 依赖于 T
pb->j++; // “pb->j” 依赖于 T
}
};
对于名称而言,对依赖名和非依赖名的查找和绑定是不同的!
2. 绑定规则
非依赖名在模板定义时进行查找和绑定。即使在模板实例时有更佳的匹配这种查找和绑定依然在定义点如期进行。
#include <iostream>
void g(double) { std::cout << "g(double)\n"; }
template<class T>
struct S
{
void f() const //函数不修改类的任何成员变量
{
g(1); // “g” 是非依赖名, 在此绑定,即实例化时绑定,查找最佳匹配
}
};
void g(int) { std::cout << "g(int)\n"; }
int main()
{
g(1); // 调用g(int)
S<int> s;
s.f(); // 调用g(double)
}
如果非依赖名称的含义在定义上下文和模板特化的实例化点之间发生变化,则程序格式不正确,无需进行诊断。以下情况下可能会出现这种情况:
• 非依赖名称中使用的类型在定义时不完整,但在实例化时完整。
• (在模板定义中查找名称时发现using声明,但在实例化中相应范围的查找未找到任何声明,因为使用声明是包扩展,而相应的包为空(自 C++17 起)。
• 实例化使用在定义时未定义的默认参数或默认模板参数。
• 实例化时的常量表达式使用整数或无作用域枚举类型的 const 对象的值、constexpr 对象的值、引用的值或 constexpr 函数的定义(自 C++11 起),并且该对象/引用/函数(自 C++11 起)在常量表达式定义时未定义。
• 模板在实例化时使用非依赖类模板特化或变量模板特化(自 C++14 起),并且它使用的模板是从在实例化时未定义的部分特化实例化的定义或命名未在定义点声明的显式特化。
依赖名的绑定被推迟到查找发生时(即查找到匹配项时绑定,因为定义时没法绑定)。
3. 查找规则
模板中使用的依赖名的查找推迟到模板参数已知时,即
• 非 ADL(Argument-dependent lookup) 查找检查模板定义上下文中可见的具有外部链接的函数声明。
• ADL 检查模板定义上下文或模板实例化上下文中可见的具有外部链接的函数声明。
(换句话说,在模板定义后添加新的函数声明不会使其可见,除非通过 ADL)。
此规则的目的是帮助防止违反模板实例化的 ODR(One Definition Rule)(一个定义原则):
// 一个外部库
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); // OK: 实例化 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'; // OK, 常规查找找到 ::operator<<
std::copy(v.begin(), v.end(),
std::ostream_iterator<elem_t>(std::cout, " "));
// 错: 来自 std::ostream_iterator 和 ADL 定义时的两种查找都仅考虑
// std 名称空间, 且将会找到很多重叠的std::operator<<, 因为会完成查找
// 则重载解析不会找到通过查找找到的集合中的elem_t的operator<<.
}
注意:在模板定义时也会进行依赖名称的有限查找(但不绑定),这是区分非依赖名称的必要条件,同时也可以确定它们是当前实例的成员还是未知特化的成员。此查找获得的信息可用于检测错误,见下文。
4. 依赖类型
下面的类型是依赖类型:
• 模板参数。
• 未知特化的成员(见下文)。
• 作为未知特化的依赖成员的嵌套类/枚举(见下文)。
• 依赖类型的 cv (const and volatile)修饰限定版本。
• 由依赖类型构造的复合类型。
• 元素类型为依赖类型或其绑定(如果有)为值依赖类型的数组类型。
• 下更类型之一的模板id:
(1) 模板名称是模板参数。
(2) 任何模板参数是类型相关的,或值相关的,或是一个包扩展(自 C++11 起)(即使使用模板 ID 时没有其参数列表,如注入类名)。
• 应用于类型相关表达式的 decltype 的结果:
应用于类型相关表达式的 decltype 的结果是唯一的依赖类型。只有当两个这样的结果的表达式相等时,它们才引用同一类型。(自 C++11 起)
• 应用于类型相关常量表达式的包索引说明符:
应用于类型相关常量表达式的包索引说明符是唯一的依赖类型。只有当两个这样的包索引说明符的常量表达式相等时,它们才引用同一类型。否则,只有当两个这样的包索引说明符的索引具有相同的值时,它们才引用同一类型。
5. 类型依赖表达式
下列表达式是依赖类型的:
• 任何子表达式都是依赖类型表达式的表达式。
• this, 若这个类是依赖类型。
• 不是concept-id 的标识符表达式且(自 C++20 起):
(1) 包含一个标识符,该标识符的名称查找至少找到一个依赖声明 。
(2) 包含一个依赖template-id 。
(3) 包含特殊标识符 __func__(如果某个封闭函数是模板、类模板的非模板成员或通用 lambda(自 C++14 起))(自 C++11 起) 。
(4) 包含转换为依赖类型的转换函数(conversion function)的名称 。
(5) 包含嵌套名称指定符或修饰id,它是未知特化的成员。
(6) 命名当前实例的依赖成员,它是“未知绑定数组”类型的静态数据成员。
(7) 包含一个标识符,该标识符的名称查找找到一个或多个当前实例的成员函数声明,这些函数声明使用返回类型推断声明(自 C++14 起)。
(8) 包含一个标识符,该标识符的名称查找找到一个结构化绑定声明,其初始化器是类型相关的。
(9) 包含一个标识符,该标识符的名称查找找到一个非类型模板参数,其类型包含占位符(placeholder) auto 。
(10) 包含一个标识符,该标识符通过名称查找找到一个声明类型包含占位符类型的变量(例如),自动静态数据成员),其中初始化器依赖于类型,(自 C++17 起)。
(11) 包含一个标识符,名称查找会找到一个包(自 C++26 起)。
(12) 任何强制转换为依赖类型的表达式。
(13) 创建依赖类型对象的 new 表达式 。
(14) 引用当前实例的成员的成员访问表达式,该成员的类型为依赖
(15) 引用未知特化的成员的成员访问表达式。
(16) 折叠表达式(fold expression)(自 C++17 起)。
(17) 如果其标识符表达式是类型相关表达式,则打包索引表达式(自 C++26 起)。
(18) 以下表达式永远不会是类型相关的,因为这些表达式的类型不能是:
文字量
伪析构函数调用
sizeof
sizeof...
alignof
noexcept(自 C++11 起)
throw
typeid
delete
requires(自 C++20 起)
6. 值依赖表达式
下列表达式是值依赖的:
• 在需要常量表达式的上下文中使用的表达式,其任何子表达式都是值依赖的。
• 满足以下任一条件的标识符表达式:
(1) 它是一个概念 id,并且它的任何参数都是值依赖的。(自 C++20 起)
(2) 它是类型依赖的。
(3) 它是非类型模板参数的名称。
(4) 它命名一个静态数据成员,该成员是当前实例的依赖成员,并且未初始化。
(5) 它命名一个静态成员函数,该函数是当前实例的依赖成员。
(6) 它是一个具有整数或枚举(直到 C++11)文字量(自 C++11 起)类型的常量,从值依赖表达式初始化。
(7) 以下表达式中的操作数是类型相关的表达式:
alignof (C++ 11之后)
(8) 在操作数是类型id依赖的下列表达式:
(9) 在目标类型或者操作数是类型依赖的下列表达式:
• C风格转换
• static_cast
• 折叠表达式
(10) 函数式强制类型转换表达式,其中目标类型是依赖的,或者值依赖表达式被括号或大括号括起来(自 C++11 起)。
(11) sizeof... 表达式,其中操作数不是结构化绑定包(自 C++26 起)
(12) 折叠表达式(自 C++17 起)
(13) 参数是修饰标识符的地址表达式,修饰表达式命名当前实例的依赖成员
(14) 参数是任何表达式的地址表达式,作为核心常量表达式进行评估,引用模板实体,该实体是具有静态或线程存储(自 C++11 起)持续时间的对象或成员函数。
7. 当前实例
在类模板定义(包括其成员函数和嵌套类)中,某些名称可能被推断为引用当前实例。这允许在定义点而不是实例点检测某些错误,并移除了对依赖名称的typename和template消歧器的要求。
只有下列名称可以引用当前实例:
• 在类模板、类模板的嵌套类、类模板的成员或类模板的嵌套类的成员的定义中:
类模板或嵌套类的注入类名。
• 在主类模板的定义或主类模板的成员中:
类模板的名称,后跟主模板的模板参数列表(或等效的别名模板特化),其中每个参数等效于(如下所定义)其对应的参数。
• 在类模板的嵌套类的定义中:
用作当前实例成员的嵌套类的名称。
• 在类模板部分特化的定义或类模板部分特化的成员的定义中:
类模板的名称,后跟部分特化的模板参数列表,其中每个参数相当于其对应的参数。
• 在模板函数的定义中:
局部类的名称。
模板实参(argument)与模板形参(parameter)等价的条件是:
• 对于类型参数,模板实参表示与模板形参相同的类型。
• 对于非类型参数,模板实参是一个命名与模板形参等价的一个变量的修饰符,变量等价于模板形参的条件是:
(1) 它具有与模板形参相同的类型(忽略 cv 限定),和
(2) 其初始化器由一个命名模板形参或递归地命名此类变量的标识符组成。
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; // OK, A<T>::M
};
将一个名称归类为当前实例的成员的条件是:
• 在当前实例或其非依赖基类中通过非限定查找找到的非限定名称。
• 限定名称,如果限定符(:: 左侧的名称)命名当前实例,并且查找在当前实例或其非依赖基中找到该名称。
• 类成员访问表达式中使用的名称(x.y 或 xp->y 中的 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(); // OK:在模板定义上下文中没有发生到类成员访问语法的转换
8. 未知特化
在模板定义中,某些名称被推断为属于未知特化,特别是:
• 限定名称,如果 :: 左侧出现的任何名称都是依赖类型,且不是当前实例的成员。
• 一个限定名称,其限定符是当前实例,并且在当前实例或其任何非依赖基类中均未找到该名称,并且存在依赖基类。
• 类成员访问表达式中成员的名称( 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; // OK: “type” 是当前实例的成员
typename A<T>::other j; // Error:
// “other” 不是当前实例的成员
// 且其不是未知特化的成员,
// 因为A<T> (其命名当前实例),
// 无 “other”依赖基本去隐藏它.
}
};
• 如果任何模板定义具有成员访问表达式,其中对象表达式是当前实例,但名称既不是当前实例的成员也不是未知特化的成员,则即使模板从未被实例化,程序也是格式错误的。
未知特化的成员始终是依赖的,并且在实例化时被查找和绑定为所有依赖名称。