On-Demand 实例化(隐式实例化)
- 编译器遇到模板特化时会用所给的实参替换对应的模板参数,从而产生特化
- 如果声明一个指向某类型的指针或引用,不需要看到类模板定义,但如果要访问特化的成员或想知道模板特化的大小,就要先看到模板定义
template<typename T>
struct A;
A<int>* p = 0;
template<typename T>
struct A {
void f();
};
void g(A<int>& a) {
a.f();
}
template<typename T>
void A<T>::f() {
}
- 函数重载时,如果候选函数的参数是类类型,则该类必须可见。如果重载函数的参数是类模板,为了检查重载匹配,就需要实例化类模板
template<typename T>
struct A {
A(int);
};
void f(A<double>) {}
void f(int) {}
int main() {
f(42);
}
延迟实例化(Lazy Instantiation)
- 实例化只会对需要的部分进行。隐式实例化模板时也实例化了每个成员声明,但没有实例化定义。但匿名 union 和虚函数例外,如果类模板包含一个匿名 union,则 union 定义的成员也被实例化了,而虚函数是否实例化依赖于具体实现
- 实例化模板时,只有当函数用上了默认实参时才会实例化该实参
template<typename T>
class Safe {
};
template<int N>
class Danger {
int arr[N];
};
template<typename T, int N>
class Tricky {
public:
void noBodyHere(Safe<T> = 3);
void inclass() {
Danger<N> noBoomYet;
}
void error() {
Danger<-1> boom;
}
void unsafe(T (*p)[N]);
T operator->();
struct Nested {
Danger<N> pfew;
};
union {
int align;
Safe<T> anonymous;
};
};
int main() {
Tricky<int, 0> ok;
}
两阶段查找(Two-Phase Lookup)
- 编译器解析模板时不能解析依赖型名称,所以编译器会在 POI(point of instantiation,实例化点)再次查找依赖型名称,而非依赖型名称在首次看到模板时就会进行查找。因此就有了两阶段查找:第一阶段发生在模板解析阶段,第二阶段在模板实例化阶段
- 第一阶段使用普通查找规则(适当情况也会用 ADL)查找非依赖型名称和非受限的依赖型名称(如函数调用中的函数名称,该名称具有依赖型实参所以是依赖型名称),但后者的查找不完整,在实例化时还会再次查找
- 第二阶段发生的地点称为 POI,这个阶段会查找依赖型受限名称,并对非受限的依赖型名称再次进行 ADL
POI(Points of Instantiation)
- 编译器会在模板中的某个位置访问模板实体的声明或定义,实例化相应的模板定义时就会产生 POI,POI 是代码中的一个点,在该点会插入替换后的模板实例
struct A {
A(int i);
};
A operator-(const A&);
bool operator>(const A&, const A&);
using Int = A;
template<typename T>
void f(T i) {
if (i > 0) g(-i);
}
void g(Int) {
f<Int>(42);
}
- 这里使用类型 A 而不是 int 的原因是,POI 执行第二次查找(查找 g(-i))使用了 ADL,int 没有关联命名空间,不会发生 ADL,也就找不到函数 g()
- 类模板实例的 POI 位置不同,它只能定义在包含该实例的声明(或定义)前的最近作用域
template<typename T>
struct A {
T x;
};
unsigned long f() {
return (unsigned long)sizeof(A<int>);
}
template<typename T>
struct A {
using Type = int;
};
template<typename T>
void f() {
A<char>::Type a = 41;
typename A<T>::Type b = 42;
}
int main() {
f<double>();
}
- 一个编译单元通常会包含一个实例的多个 POI,对类模板实例,每个编译单元只有首个 POI 会被保留,其他 POI 会被忽略(它们不会被真正认为是 POI),对于函数模板和变量模板的实例,所有 POI 都会保留。ODR 原则要求在保留的任何一个 POI 位置出现的实例化体等价,但编译器没有这个约束,因此编译器允许选择一个 non-class 类型的 POI 执行实例化,而不用担心其他 POI 产生不同的实体
显式实例化
- 为模板特化显式生成 POI 的构造称为显式实例化指示符,它由 template 关键字和特化声明组成
template<typename T>
void f(T) {}
template void f<int>(int);
template void f<>(float);
template void f(long);
- 类模板成员也可以显式实例化,显式实例化一个类也会实例化所有成员
template<typename T>
class A {
public:
void f() {}
};
template class A<void>;
template void A<int>::f();
template<typename T>
void f() {
}
template void f<int>();
template<>
void f<int>() {
std::cout << 1;
}
#include <iostream>
template<typename T>
void f() {
}
template<>
void f<int>() {
std::cout << 1;
}
template void f<int>();
int main() {
f<int>();
}
- 显式实例化不会影响类型推断规则,它只是实例化了一个实体,并不是一个可以优先匹配的非模板函数。从函数模板实例化而来的函数永远不和普通函数等价
template<typename T>
void f(T, T) {}
template void f<double>(double, double);
f(1, 3.14);
f<double>(1, 3.14);
显式实例化声明(Explicit Instantiation Declarations)
- 由关键字 extern 指定的显式实例化称为显式实例化声明,它会抑制隐式实例化,但以下情况例外:
- 内联函数为了展开内联,仍能被实例化
- auto 或 decltype(auto) 类型变量和函数返回类型,仍可以被实例化来确定类型
- 值为常量表达式的变量仍能被实例化以计算它们的值
- 引用类型变量仍能被实例化,这样引用的实体才能被解析
- 类模板和别名模板,为了检查生成类型仍能被实例化
- 使用显式实例化声明,可以在头文件中提供模板定义,以此抑制隐式实例化的特化。每个显式实例化声明必须与定义配对,省略定义将引发链接错误
template<typename T> void f() {}
extern template void f<int>();
extern template void f<float>();
template void f<int>();
template void f<float>();
- 当特化被用在许多不同的编译单元中,显式实例化声明能用来提高编译效率。不同于手动实例化每需要一个新的特化都要手动更新显式实例化定义列表,显式实例化声明能在任何情况下作为一个优化引入。然而编译时间优化上就不如手动实例化了,因为可能产生一些冗余的隐式实例化,且模板定义也会被作为头文件的一部分解析
标准库中的显式实例化
- 标准库就有使用显式实例化的例子,如 basic_iostream 常用于char 或 wchar_t,标准库的实现就会为这些常见情况引入显式实例化声明
#if defined(_DLL_CPPLIB)
#if !defined(_CRTBLD) || defined(__FORCE_INSTANCE)
template class _CRTIMP2_PURE_IMPORT basic_iostream<char, char_traits<char>>;
template class _CRTIMP2_PURE_IMPORT basic_iostream<wchar_t, char_traits<wchar_t>>;
#endif
#ifdef __FORCE_INSTANCE
template class _CRTIMP2_PURE_IMPORT basic_iostream<unsigned short, char_traits<unsigned short>>;
#endif
#endif
if constexpr
- C++17 引入了编译期 if,条件为 false 的分支会被丢弃而不会实例化
template<typename T, typename... Ts>
void print(T&& t, Ts&&... ts) {
std::cout << t << std::endl;
if constexpr (sizeof...(ts) > 0) {
print(std::forward<Ts>(ts)...);
}
}
int main() {
print(3.14, 42, std::string{ "hello" }, "world");
}
- C++17 之前没有 if constexpr,需要用特化或重载实现类似的功能
template<bool b>
struct A;
template<typename T, typename... Ts>
void print(T&& t, Ts&&... ts) {
std::cout << t << std::endl;
A<(sizeof...(ts) > 0)>::f(std::forward<Ts>(ts)...);
}
template<bool b>
struct A {
template<typename... Ts>
static void f(Ts&&... ts) {
print(std::forward<Ts>(ts)...);
}
};
template<>
struct A<false> {
template<typename... Ts>
static void f(Ts&&... x) {}
};
int main() {
print(3.14, 42, std::string{ "hello" }, "world");
}
- if constexpr 能用于任何函数而不仅局限于模板,但判断的必须是编译期表达式
void f();
void g() {
if constexpr (sizeof(int) == 1) {
f();
}
}