模板实例化过程:在编译期间通过替换模板参数获取到一个类型或者函数的过程,其中涉及到模板实参推导等过程,本文主要介绍模板实例化的相关基础;
延迟实例化
引用【1】中的代码示例进行说明:
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) // OK until usage of default value results in an error
{
}
void inclass() {
Danger<N> noBoomYet; // OK until inclass() is used with N <= 0
}
struct nested {
Danger<N> pfew; // OK until nested() is used with N <= 0
};
union {
Danger<N> anonymous; // OK until Tricky is instantiated with N <= 0
int align;
};
void unsafe(T (*p)[N]); // OK until Tricky is instantiated with N <= 0
void error(){
Danger<-1> boom; // 根据编译器有所差别
}
void process()
{
std::cout << __FUNCTION__ << std::endl;
}
};
在模板进行隐式实例化时:
Tricky<int, 1> t{}; // 编译成功
Tricky<int, -1> t{}; // 编译失败
cpp_projects/test/test_code/main.cpp: In instantiation of ‘class Danger<-1>’:
cpp_projects/test/test_code/main.cpp:51:17: required from ‘union Tricky<int, -1>::<unnamed>’
cpp_projects/test/test_code/main.cpp:50:4: required from ‘class Tricky<int, -1>’
cpp_projects/test/test_code/main.cpp:109:18: required from here
cpp_projects/test/test_code/main.cpp:33:7: error: size ‘18446744073709551615’ of array exceeds maximum object size ‘9223372036854775807’
33 | int arr[N];
| ^~~
cpp_projects/test/test_code/main.cpp: In instantiation of ‘class Tricky<int, -1>’:
cpp_projects/test/test_code/main.cpp:109:18: required from here
cpp_projects/test/test_code/main.cpp:55:14: error: size ‘18446744073709551615’ of array exceeds maximum object size ‘9223372036854775807’
55 | void unsafe(T (*p)[N]);
| ^~~~~~
cpp_projects/test/test_code/main.cpp:55:25: error: size ‘18446744073709551615’ of array exceeds maximum object size ‘9223372036854775807’
55 | void unsafe(T (*p)[N]);
| ~~~~^~~~~
make[2]: *** [test/test_code/CMakeFiles/test_code.dir/build.make:82: test/test_code/CMakeFiles/test_code.dir/main.cpp.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:584: test/test_code/CMakeFiles/test_code.dir/all] Error 2
这里实例化时注意几个编译器的规则:
1)类内的函数声明等会进行实例化(partial instantiation),但不会去检查实现进行全实例化(full instantiation),除非进行了函数调用;
2)类内的定义在没有使用时不会实例化,除了匿名union除外;
3)函数中的默认参数不会进行实例化,如上述代码中的Safe<T> = 3;除非进行了函数调用;
4)process 由于不会调用,所以实例化不会生成对应的代码段;
当非类型模板参数为-1时:
1)由于类内函数声明会进行实例化,所以导致void unsafe(T (*p)[N])编译报错;
2)union会进行实例化,所以Danger<N> anonymous 也会编译报错;而nested、inclass作为类内定义,由于没有使用,所以不会实例化从而也就不会检查Danger<N>;
3)函数默认参数Safe<T> 不会进行实例化,并且由于函数未调用,Safe<T> = 3赋值不会进行check,所以不会有编译报错;
所以上述隐式实例化时会有两个编译错误;
通过cppinsights可以看到实例化之后的代码:
从实例化后的代码也说明了延迟实例化的特点:"Only as much as is really needed",例如上述代码中process函数;
而error函数中会由于编译器选择可能不会报错;
void error(){
Danger<-1> boom; // 根据编译器有所差别
}
默认参数
修改隐式实例化为:
Tricky<int, 1> t{};
t.noBodyHere();
/home/insights/insights.cpp:15:29: error: no viable conversion from 'int' to 'Safe<int>'
void noBodyHere(Safe<T> =3);
此时由于对noBodyHere进行了调用,因此Safe<T>进行了实例化,int无法转换到Safe<int>,导致编译失败;
进一步修改代码为:
Tricky<int, 1> t{};
t.noBodyHere(Safe<int> {});
此时不会使用默认参数,编译通过;
类内虚函数
template<typename T>
class VirtualClass{
public:
virtual ~VirtualClass(){};
virtual T vmem(); //#1
};
此时在编译期不会出错,但是链接时会找不到vmem的实现而报错,即类模板实例化时类内的所有虚函数必须进行全实例化,不管此时虚函数是否使用;这里和虚函数表机制相关,后续单独进行说明;
两阶段查找(two phase lookup):
1)阶段一:解析模板,对于非依赖模板参数的名称,应用常规查找规则+参数依赖查找规则(ADL),对于依赖模板参数的名称除了常规查找,还需要在第二阶段在模板实例化之后应用ADL查找;
2)阶段二:在模板实例化之后,应用ADL查找名称;
其中ADL可以参考笔者之前的介绍【2】,分析如下代码:
namespace N {
template<typename T> void g(T p){}
enum E{ e };
}
template<typename T> void f(T p){}
template<typename T> void h(T p)
{
f<int>(p); //#1
g<int>(p); //#2
}
int main()
{
h(N::e);
}
在h实例化前,常规查找可以找到函数f,但不能找到函数g;h实例化后,入参类型T为N::E,在二阶段应用ADL,可以找到命名空间N内的函数g;
在实例化时编译器会选择在代码中插入实例化后的类,选择的位置称为Points of Instantiation
显示实例
除了隐式通过模板参数推导进行实例化,同样也可以进行显示实例化,以函数模板为例:
template<typename T>
void process(T)
{}
template void process<int>(int);
template void process<>(float);
template void process(char);
查看编译后生成的代码,可以看到即使main函数中没有调用,也会生成process实例化后的代码;
类模板同样如此,并且类模板显示实例化会将类内定义、和函数定义都进行实例化;而延迟实例化为调用时才会进行实例化;
编译构建
通常模板实现在头文件中,需要编译的单元包含头文件进行实例化,对于相同模板参数链接器最终留下一份模板实例,不同编译单元对于相同模板参数反复实例化也会对编译构建造成开销,尤其在模板实现复杂的时候,这也是不推荐将复杂实现放在模板定义中的原因,一来引起代码膨胀,二来构建时间变长;
常用的解决方案将模板定义与声明分开,将模板定义隐藏;此时用户只需要包含TestClass.h,编译时发现TestClass<int>并且当前编译单元找不到完整定义,则会留到链接阶段从其他编译单元中找到对应实现;由于TestClass.cpp进行了显示实例化,编译单元中包含了TestClass<int>的实现,因此user中的调用链接成功;
参考资料
【1】C++ Templates
【2】C++ ADL & CPO介绍