模板全特化
本节将介绍模板全特化。
其实很多东西都能进行全特化,不过我们围绕着之前的内容:函数模板、类模板、变量模板来展开。
函数模板全特化
给出这样一个函数模板 f
,你可以看到,它的逻辑是返回两个对象相加的结果,那么如果我有一个需求:“如果我传的是一个 double 一个 int 类型,那么就让它们返回相减的结果”。
template<typename T,typename T2>
auto f(const T& a, const T2& b) {
return a + b;
}
C++14 允许函数返回声明的 auto 占位符自行推导类型。
这种定制的需求很常见,此时我们就需要使用到模板全特化:
template<>
auto f(const double& a, const int& b){
return a - b;
}
当特化函数模板时,如果模板实参推导能通过函数实参提供,那么就可以忽略它的模板实参。
语法很简单,只需要先写一个 template<>
后面再实现这个函数即可。
不过我们其实有两种写法的,比如上面那个示例,我们还可以写明模板实参。
template<>
auto f<double, int>(const double& a, const int& b) {
return a - b;
}
个人建议写明更加明确,因为很多时候模板实参只是函数形参类型的一部分而已,比如上面的 const double&
、const int&
只有 double
、int
是模板实参。
使用:
std::cout << f(2, 1) << '\n'; // 3
std::cout << f(2.1, 1) << '\n'; // 1.1
类模板全特化
和函数模板一样,类模板一样可以进行全特化。
template<typename T> // 主模板
struct is_void{
static constexpr bool value = false;
};
template<> // 对 T = void 的显式特化
struct is_void<void>{
static constexpr bool value = true;
};
int main(){
std::cout <<std::boolalpha<< is_void<char>::value << '\n'; // false
std::cout << std::boolalpha << is_void<void>::value << '\n'; // true
}
我们使用全特化,实现了一个 is_void
判断模板类型实参是不是 void
类型。
虽然很简单,但我们还是稍微强调一下:同一个类模板实例化的不同的类,彼此之间毫无关系,而静态数据成员是属于类的,而不是模板类;模板类实例化的不同的类,它们的静态数据成员不是同一个,请注意。
我们知道标准库在 C++17
引入了 is_xxx
的 _v
的版本,就不需要再写 ::value
了。所以我们也可以这么做,这会使用到变量模板。
#include <iostream>
template<typename T> // 主模板
struct is_void{
static constexpr bool value = false;
};
template<> // 对 T = void 的显式特化
struct is_void<void>{
static constexpr bool value = true;
};
template<typename T>
constexpr bool is_void_v = is_void<T>::value;
int main(){
std::cout <<std::boolalpha<< is_void_v<char> << '\n'; // false
std::cout << std::boolalpha << is_void_v<void> << '\n'; // true
}
我们再给出一个简单的示例:
template<typename T>
struct X{
void f()const{
puts("f");
}
};
template<>
struct X<int>{
void f()const{
puts("X<int>");
}
void f2()const{}
int n;
};
int main(){
X<void> x;
X<int> x_i;
x.f(); // 打印 f
//x.f2(); // Error!
x_i.f(); // 打印 X<int>
x_i.f2();
}
我们要明白,写一个类的全特化,就相当于写一个新的类一样,你可以自己定义任何东西,不管是函数、数据成员、静态数据成员,等等;根据自己的需求。
变量模板全特化
#include <iostream>
template<typename T>
constexpr const char* s = "??";
template<>
constexpr const char* s<void> = "void";
template<>
constexpr const char* s<int> = "int";
int main(){
std::cout << s<void> << '\n'; // void
std::cout << s<int> << '\n'; // int
std::cout << s<char> << '\n'; // ??
}
语法形式和前面函数模板、类模板都类似,很简单,这个变量模板是类型形参。我们特化了变量模板 s
的模板实参为 void
与 int
的情况,修改 s
的初始化器,让它的值不同。
template<typename T>
constexpr bool is_void_v = false;
template<>
constexpr bool is_void_v<void> = true;
int main(){
std::cout << std::boolalpha << is_void_v<char> << '\n'; // false
std::cout << std::boolalpha << is_void_v<void> << '\n'; // true
}
上面的变量模板,模板是类型形参,我们根据类型进行全特化。我们特化了 is_void_v
的模板实参为 void
的情况,让 is_void_v
值 为 true
。
细节
前面函数、类、变量模板的全特化都讲的很简单,示例也很简单,或者说语法本身大多数时候就是简单的。我们在这里进行一些更多的补充一些细节。
特化必须在导致隐式实例化的首次使用之前,在每个发生这种使用的翻译单元中声明:
template<typename T> // 主模板
void f(const T&){}
void f2(){
f(1); // 使用模板 f() 隐式实例化 f<int>
}
template<> // 错误 f<int> 的显式特化在隐式实例化之后出现
void f<int>(const int&){}
如果 f2 中的调用换成 f(1.)
就没问题,它隐式实例化的就是 f<double>
了。
只有声明没有定义的模板特化可以像其他不完整类型一样使用(例如可以使用到它的指针和引用):
template<class T> // 主模板
class X;
template<> // 特化(声明,不定义)
class X<int>;
X<int>* p; // OK:指向不完整类型的指针
X<int> x; // 错误:不完整类型的对象
函数模板和变量模板的显式特化是否为 inline/constexpr/constinit/consteval 只与显式特化自身有关,主模板的声明是否带有对应说明符对它没有影响。模板声明中出现的属性在它的显式特化中也没有效果:
template<typename T>
int f(T) { return 6; }
template<>
constexpr int f<int>(int) { return 6; } // OK,f<int> 是以 constexpr 修饰的
template<class T>
constexpr T g(T) { return 6; } // 这里声明的 constexpr 修饰函数模板是无效的
template<>
int g<int>(int) { return 6; } //OK,g<int> 不是以 constexpr 修饰的
int main(){
constexpr auto n = f<int>(0); // OK,f<int> 是以 constexpr 修饰的,可以编译期求值
//constexpr auto n2 = f<double>(0); // Error! f<double> 不可编译期求值
//constexpr auto n3 = g<int>(0); // Error! 函数模板 g<int> 不可编译期求值
constexpr auto n4 = g<double>(0); // OK! 函数模板 g<double> 可编译期求值
}
如果主模板有 constexpr 属性,那么模板实例化的,如 g<double>
自然也是附带了 constexpr
,但是如果其特化没有,那么以特化为准(如 g<int>
)。
特化的成员
特化成员的写法略显繁杂,但是只要明白其逻辑,一切就会很简单。
主模板
template<typename T>
struct A{
struct B {}; // 成员类
template<class U> // 成员类模板
struct C {};
};
特化模板类。
A<void>
。
template<>
struct A<void>{
void f(); // 类内声明
};
void A<void>::f(){ // 类外定义
// todo..
}
特化成员类。设置
A<char>
的情况下B
类的定义。
template<>
struct A<char>::B{ // 特化 A<char>::B
void f(); // 类内声明
};
void A<char>::B::f(){ // 类外定义
// todo..
}
特化成员类模板。设置
A<int>
情况下模板类C
的定义。
template<>
template<class U>
struct A<int>::C{
void f(); // 类内声明
};
// template<> 会用于定义被特化为类模板的显式特化的成员类模板的成员
template<>
template<class U>
void A<int>::C<U>::f(){ // 类外定义
// todo..
}
特化类的成员函数模板
其实语法和普通特化函数模板没什么区别,类外的话那就指明函数模板是在 X 类中。
struct X{
template<typename T>
void f(T){}
template<> // 类内特化
void f<int>(int){
std::puts("int");
}
};
template<> // 类外特化
void X::f<double>(double){
std::puts("void");
}
X x;
x.f(1); // int
x.f(1.2); // double
x.f("");
特化类模板的成员函数模板
成员或成员模板可以在多个外围类模板内嵌套。在这种成员的显式特化中,对每个显式特化的外围类模板都有一个 template<>
。
其实就是注意有几层那就多套几个 template<>
,并且指明模板类的模板实参。下面这样:就是自定义了 X<void>
且 f<double>
的情况下的函数。
template<typename T>
struct X {
template<typename T2>
void f(T2) {}
template<>
void f<int>(int) { // 类内特化,对于 函数模板 f<int> 的情况
std::puts("f<int>(int)");
}
};
template<>
template<>
void X<void>::f<double>(double) { // 类外特化,对于 X<void>::f<double> 的情况
std::puts("X<void>::f<double>");
}
X<void> x;
x.f(1); // f<int>(int)
x.f(1.2); // X<void>::f<double>
x.f("");
视频中的代码,模板类和成员函数模板都用的
T
,只能在msvc
下运行,gcc 与 clang 有歧义,需要注意。
类内对成员函数
f
的特化,在gcc
无法通过编译,根据考察,这是一个很多年前就有的BUG
,使用gcc
的开发者自行注意。
总结
我们省略了一些内容,但是以上在我看来也完全足够各位学习使用了。如有需求,查看 cppreference。
模板全特化的语法主要核心在于 template<>
,以及你需要注意,你到底要写几个 template<>
。其他的都很简单。