变长参数模板
解释
C++03只有固定模板参数。C++11 加入新的表示法,允许任意个数、任意类别的模板参数,不必在定义时将参数的个数固定。
变长模板、变长参数是依靠C++11新引入的参数包的机制实现的。
参数包
-
一个模板形参包(template parameter pack)是一个接受零个或多个模板实参的模板形参。
template<class ... Types> struct Tuple { };
Tuple<> t0; // Types不含任何实参
Tuple<int> t1; // Types含有一个实参:int
Tuple<int, float> t2; // Types含有两个实参:int和float
Tuple<0> error; // 错误:0不是一个类型
-
一个函数形参包(function parameter pack)是一个接受零个或多个函数实参的函数形参
template<class ... Types> void f(Types... args);
f(); // OK:args不含有任何实参
f(1); // OK:args含有一个实参:int
f(2, 1.0); // OK:args含有两个实参int和double
-
一个形参包要么是一个模板形参包,要么是一个函数形参包。
-
一个包扩展(expansion)由一个模式(pattern)和一个省略号组成。包扩展的实例中一个列表中产生零个或多个模式的实例。模式的形式依赖于扩展所发生的上下文中
template <typename... TS> // typename... TS为模板形参包,TS为模式
static void MyPrint(const char* s, TS... args) // TS... args为函数形参包,args为模式
{
printf(s, args...);
}
解包
一个常用的技巧是:利用模板推导机制,每次从参数包里面取第一个元素,缩短参数包,直到包为空。
template <typename T>
void fun(const T& t){
cout << t << '\n';
}
template <typename T, typename ... Args>
void fun(const T& t, Args ... args){
cout << t << ',';
fun(args...);//递归解决,利用模板推导机制,每次取出第一个,缩短参数包的大小。
}
在C++17标准中,可以使用fold expression,更直接地表达,并且确保正序展开:
// C++17
template<typename T0, typename... T>
void printf(T0 t0, T... t) {
std::cout << t0 << std::endl;
if constexpr (sizeof...(t) > 0) printf(t...);
}
// C++11
#include <iostream>
template <typename T0>
void printf(T0 value){
std::cout << value << std::endl;
}
template <typename T, typename... Args>
void printf(T value, Args... args){
std::cout << value << std::endl;
printf(args...);
}
int main(){
printf(1, 2, "123", 1.1);
return 0;
}
外部模板
关键词
extern
语法
extern template class|struct 模板名 < 实参列表 > ;
解释
类模板自身并不是类型、对象或任何其他实体。不会从仅含模板定义的源文件生成任何代码。必须实例化模板以令任何代码出现:必须提供模板实参,使得编译器能生成实际的类(或从函数模板生成函数)。
如果外部模板声明出现于某个编译单元中,那么与之对应的显式实例化必须出现于另一个编译单元中或者同一个编译单元的后续代码中;
外部模板不能用于一个静态函数(没有外部链接属性),但可以用于类静态成员函数。
类模板实例化分为两种:显示实例化和隐式实例化。
显示实例化有如下方法:
template class|struct 模板名 < 实参列表 > ;
extern template class|struct 模板名 < 实参列表 > (C++11 起);
第二种方法就是我们要说的C++11新增的方法。
隐式实例化:
当代码在要求完整定义的类型的语境中涉指某个模板时,或当类型的完整性对代码有影响,而这个特定类型尚未被显式实例化时,发生隐式实例化。例如当构造此类型的对象之时,但不包括构造指向此类型的指针之时。举个例子:
template<class T> struct Z {
void f() {}
void g(); // 并不定义
}; // 模板定义
template struct Z<double>; // 显式实例化 Z<double>
Z<int> a; // 隐式实例化 Z<int>
Z<char>* p; // 此处不实例化任何内容
p->f(); // 隐式实例化 Z<char> 而 Z<char>::f() 出现于此。
// 并不需要且始终不实例化 Z<char>::g():不必对其进行定义
WHY
而对于函数模板来说,现在我们遇到的问题和extern一个变量遇到的问题相同。不同的是,发生问题的不是变量(数据),而是函数(代码)。这样的困境是由于模板的实例化带来的。
比如,我们在一个test.h的文件中声明了如下一个模板函数:
template <typename T> void fun(T) {}
在第一个test1.cpp文件中,我们定义了以下代码:
#include "test.h"
void test1() { fun(3); }
而在另一个test2.cpp文件中,我们定义了以下代码:
#include "test.h"
void test2() { fun(4); }
由于两个源代码使用的模板函数的参数类型一致,所以在编译test1.cpp的时候,编译器实例化出了函数 fun(int),而当编译test2.cpp的时候,编译器又再一次实例化出了函数fun(int)。那么可以想象,在test1.o目标文件和test2.o目标文件中,会有两份一模一样的函数fun(int)代码。
代码重复和数据重复不同。数据重复,编译器往往无法分辨是否是要共享的数据;而代码重复,为了节省空间,保留其中之一就可以了(只要代码完全相同)。事实上,大部分链接器也是这样做的。在链接的时候,链接器通过一些编译器辅助的手段将重复的模板函数代码fun(int)删除掉,只保留了单个副本。这样一来,就解决了模板实例化时产生的代码冗余问题。
不过读者也注意到了,对于源代码中出现的每一处模板实例化,编译器都需要去做实例化的工作;而在链接时,链接器还需要移除重复的实例化代码。很明显,这样的工作太过冗余,而在广泛使用模板的项目中,由于编译器会产生大量冗余代码,会极大地增加编译器的编译时间和链接时间。解决这个问题的方法基本跟变量共享的思路是一样的,就是使用“外部的”模板。
C++11我们可以通过下面代码来实现显示实例化:
extern template void fun<int>(int);
这样一来,在test2.o中不会再生成fun(int)的实例代码。由于test2.o不再包含fun(int)的实例,因此链接器的工作很轻松,基本跟外部变量的做法是一样的,即只需要保证让test1.cpp和test2.cpp共享一份代码位置即可。而同时,编译器也不用每次都产生一份fun(int)的代码,所以可以减少编译时间。这里也可以把外部模板声明放在头文件中,这样所有包含test.h的头文件就可以共享这个外部模板声明了。这一点跟使用外部变量声明是完全一致的。