模板特化
模板特化的核心作用就是,让通用的模板能够在某些特定场景下实现“定制化”。
全特化
举个例子,我们定义了这样一个 Printer
:
template<typename T>
class Printer {
public:
void print(const T& obj) {
std::cout << "Default print: " << obj << std::endl;
}
};
显然对于大部分的基本类型,这个模板类都能够正确地打印出结果,比如
Printer<float> printer1;
float a = 114.514;
printer1.print(a); // Default print: 114.514
Printer<int> printer2;
int b = 1919810;
printer2.print(b); // Default print: 1919810
Printer<std::string> printer3;
std::string c = "hello";
printer3.print(c); // Default print: hello
但是如果这里的 obj
是一个 vector<int>
类型呢?显然这个模板不能直接把 vector
类型打印出来,这就说明模板虽然能够cover大部分的通用情况,但是对于某些特例,我们需要特殊处理,这也就是模板特化的概念了。
针对 vector<int>
的模板全特化,我们可以这么实现:
template<>
class Printer<std::vector<int>> {
public:
void print(std::vector<int>& v) {
std::cout << "Vector print:";
for (const auto& i : v) {
std::cout << " " << i;
}
std::cout << std::endl;
}
};
这里 template<>
表示我们要对模板进行全特化,class Printer<std::vector<int>>
中的尖括号 <std::vector<int>>
表示这个特化对于 vector<int>
这种类型生效。
调用:
std::vector vec{114, 514, 1919, 810};
Printer<std::vector<int>> printer4;
printer4.print(vec); // Vector print: 114 514 1919 810
存在模板全特化时,编译器就会优先尝试匹配全特化的类型。
偏特化
还是举个例子,假设我们有这么一个 Pair
类:
template<typename T, typename U>
class Pair {
public:
T first;
U second;
Pair(T f, U s) : first(f), second(s) {}
void show() {
std::cout << "Default show: (" << first << ", " << second << ")\n";
}
};
然后可以有以下调用:
Pair pr1(114, 514);
pr1.show(); // Default show: (114, 514)
Pair pr2("hello", 233.33);
pr2.show(); // Default show: (hello, 233.33)
int x = 810;
Pair pr3(1919, &x);
pr3.show(); // Default show: (1919, 00000037D0F4F5A4)
不难发现,这里的 pr3
输出了地址,但我们想要输出该地址的值怎么办?明确一下问题:我们有这样一个默认的模板样式:<T, U>
,但是现在我们希望对于 <T, U*>
进行特化。注意到这里不是全特化,因为 T
仍然是默认的模板参数,我们只对于 U*
进行了模板特化,这就是模板偏特化的概念(只对部分模板参数进行特化)。
template<typename T, typename U>
class Pair<T, U*> {
public:
T first;
U* second;
Pair(T f, U* s) : first(f), second(s) {}
void show() {
std::cout << "<T, U*> show: (" << first << ", " << *second << ")\n";
}
};
这个案例可以按照上方代码实现偏特化与全特化不同的是 template<typename T, typename U>
不变(全特化的语法是 template<>
),class Pair
后面的尖括号内填入需要偏特化的模板参数 <T, U*>
,
Pair pr3(1919, &x);
pr3.show(); // <T, U*> show: (1919, 810)
模板偏特化是一个很强大的工具,在全特化的例子中,我们对于 vector<int>
类型实现了全特化,但是更多场景下,我们实际上希望对于 vector<ElemType>
进行特化(这里的 ElemType
可能是任意的基础类型),此时就可以采用偏特化实现:
template<typename ElemType>
class Printer<std::vector<ElemType>> {
public:
void print(std::vector<ElemType>& v) {
std::cout << "Vector print:";
for (const auto& i : v) {
std::cout << " " << i;
}
std::cout << std::endl;
}
};
全特化 vs 偏特化
- 全特化:针对模板参数的全部类型做精确匹配——只有和它一模一样的参数列表才会走这套实现。
- 偏特化:针对参数模式(pattern)做“模糊”匹配——可以匹配一大类类型(如所有指针、数组、容器等)。
此外容易注意到,偏特化的语义显然广于全特化,因此编译器会优先尝试全特化,如果不匹配再尝试偏特化,也就是全特化 > 最匹配的偏特化 > 原始通用模板。
变参模板
递归展开
假设需要实现一个类似于python中的 print
函数,这种函数需要接收不定数量和类型的参数,这种时候就需要用到变参模板了。
void print() {
std::cout << "\n";
}
template<typename T, typename... Args>
void print(const T& t, const Args&... args) {
std::cout << t << " ";
print(args...);
}
- 这里的
typename... Args
就是变参模板的语法 - 比如我要
print(a, b, c);
,那么这个变参模板的调用会通过print(T t, Args... args)
先将a
打印出来,然后递归进入print(b, c)
…… - 当递归调用到
print(void)
时,就会进入我们定义的递归出口void print()
不难发现变参模板在使用上通常需要定义一个递归函数和一个递归出口函数。
折叠表达式(c++17)
引入
template<typename... Args>
void print(const Args&... args) {
((std::cout << args << " "), ...);
std::cout << "\n";
}
- 这里的
((std::cout << args << " "), ...)
就是折叠表达式,注意最外层一定要用括号包起来(让编译器识别出这是折叠表达式),内部的...
就是在展开args
。
template<typename... Args>
void print(const Args&... args) {
(..., (std::cout << args << " "));
std::cout << "\n";
}
- 这就变成了一个右折叠表达式(优先从右侧参数开始展开)。
折叠表达式还能实现很多功能,比如这么一个求和函数:
template<typename... Args>
auto sum(Args... args) -> decltype((args + ...)) {
return (args + ...)
}
- 这个求和函数中用到了
decltype
这个不常用的关键字,但在这个模板中确是必需的,因为模板函数的返回值是你无法确定的,因此返回类型必须是auto
,让编译器通过decltype
自动推导 - 此外,注意到
decltype
的内部是(args + ...)
,这仍然是一个折叠表达式,原因是Args
是一个变参模板,内部可能有各种不同类型,我们必须通过该函数的返回值类型进行类型推导。
具体语法
折叠表达式具体可以分为左折叠和右折叠,这里不详细展开具体语法定义,简单说明一下折叠表达式展开的理解,实际上折叠表达式就是根据二元运算符 op
和参数包 args
的位置对 args
进行展开,比如说对于
((std::cout << args << " "), ...);
这个表达式就会展开为
(((std::cout << args[1]), std::cout << args[2]), std::cout << args[3]);
等效于std::cout << 1, std::cout << 2, std::cout << 3;
如果是
(..., (std::cout << args << " "));
这个表达式就会展开为
(std::cout << 1, (std::cout << 2, (std::cout << 3)));
也等效于std::cout << 1, std::cout << 2, std::cout << 3;
- 这里的折叠表达式中的二元运算符是
,
- 如果二元运算符
op
在args
右侧,那么就是先展开左侧参数(args op ...
);反之亦然。