Skip to content

模板全特化

本节将介绍模板全特化。

其实很多东西都能进行全特化,不过我们围绕着之前的内容:函数模板、类模板、变量模板来展开。

函数模板全特化

给出这样一个函数模板 f,你可以看到,它的逻辑是返回两个对象相加的结果,那么如果我有一个需求:“如果我传的是一个 double 一个 int 类型,那么就让它们返回相减的结果”。

cpp
template<typename T,typename T2>
auto f(const T& a, const T2& b)  {
    return a + b;
}

C++14 允许函数返回声明的 auto 占位符自行推导类型。

这种定制的需求很常见,此时我们就需要使用到模板全特化:

cpp
template<>
auto f(const double& a, const int& b){
    return a - b;
}

当特化函数模板时,如果模板实参推导能通过函数实参提供,那么就可以忽略它的模板实参

语法很简单,只需要先写一个 template<> 后面再实现这个函数即可。


不过我们其实有两种写法的,比如上面那个示例,我们还可以写明模板实参。

cpp
template<>
auto f<double, int>(const double& a, const int& b) {
    return a - b;
}

个人建议写明更加明确,因为很多时候模板实参只是函数形参类型的一部分而已,比如上面的 const double&const int& 只有 doubleint 是模板实参。

使用

cpp
std::cout << f(2, 1) << '\n';    // 3
std::cout << f(2.1, 1) << '\n';  // 1.1

类模板全特化

和函数模板一样,类模板一样可以进行全特化。

cpp
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 了。所以我们也可以这么做,这会使用到变量模板。

cpp
#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
}

我们再给出一个简单的示例:

cpp
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();
}

我们要明白,写一个类的全特化,就相当于写一个新的类一样,你可以自己定义任何东西,不管是函数、数据成员、静态数据成员,等等;根据自己的需求。

变量模板全特化

cpp
#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 的模板实参为 voidint 的情况,修改 s 的初始化器,让它的值不同。

cpp
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

细节

前面函数、类、变量模板的全特化都讲的很简单,示例也很简单,或者说语法本身大多数时候就是简单的。我们在这里进行一些更多的补充一些细节


特化必须在导致隐式实例化的首次使用之前,在每个发生这种使用的翻译单元中声明:

cpp
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> 了。


只有声明没有定义的模板特化可以像其他不完整类型一样使用(例如可以使用到它的指针和引用):

cpp
template<class T> // 主模板
class X;
template<>        // 特化(声明,不定义)
class X<int>;
 
X<int>* p;       // OK:指向不完整类型的指针
X<int> x;        // 错误:不完整类型的对象

函数模板和变量模板的显式特化是否为 inline/constexpr/constinit/consteval 只与显式特化自身有关主模板的声明是否带有对应说明符对它没有影响。模板声明中出现的属性在它的显式特化中也没有效果:

cpp
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>)。


特化的成员

特化成员的写法略显繁杂,但是只要明白其逻辑,一切就会很简单。

主模板

cpp
template<typename T>
struct A{
    struct B {};      // 成员类

    template<class U> // 成员类模板
    struct C {};
};

特化模板类A<void>

cpp
template<>
struct A<void>{
    void f();       // 类内声明
};

void A<void>::f(){  // 类外定义
    // todo..
}

特化成员类。设置 A<char> 的情况下 B 类的定义。

cpp
template<>              
struct A<char>::B{      // 特化 A<char>::B
    void f();           // 类内声明
};

void A<char>::B::f(){   // 类外定义
    // todo..    
}

特化成员类模板。设置 A<int> 情况下模板类 C 的定义。

cpp
template<>
template<class U>
struct A<int>::C{
    void f();               // 类内声明
};
// template<> 会用于定义被特化为类模板的显式特化的成员类模板的成员
template<>
template<class U>
void A<int>::C<U>::f(){     // 类外定义
    // todo..
}

特化类的成员函数模板

其实语法和普通特化函数模板没什么区别,类外的话那就指明函数模板是在 X 类中。

cpp
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> 的情况下的函数。

cpp
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<>。其他的都很简单。