Skip to content

模板偏特化

如果你认真学习了我们上一节内容,本节应当是十分简单的。

模板偏特化这个语法让模板实参具有一些相同特征可以自定义,而不是像全特化那样,必须是具体的什么类型,什么值。

比如:指针类型,这是一类类型,有 int*double*char*,以及自定义类型的指针等等,它们都属于指针这一类类型;可以使用偏特化对指针这一类类型进行定制。

  • 模板偏特化使我们可以对具有相同的一类特征的类模板、变量模板进行定制行为。

变量模板偏特化

cpp
template<typename T>
const char* s = "?";            // 主模板

template<typename T>
const char* s<T*> = "pointer";  // 偏特化,对指针这一类类型

template<typename T>
const char* s<T[]> = "array";   // 偏特化,但是只是对 T[] 这一类类型,而不是数组类型,因为 int[] 和 int[N] 不是一个类型

std::cout << s<int> << '\n';            // ?
std::cout << s<int*> << '\n';           // pointer
std::cout << s<std::string*> << '\n';   // pointer
std::cout << s<int[]> << '\n';          // array
std::cout << s<double[]> << '\n';       // array
std::cout << s<int[1]> << '\n';         // ?

语法就是正常写主模板那样,然后再定义这个 s 的时候,指明模板实参。或者你也可以定义非类型的模板形参的模板,偏特化,都是一样的写法。

不过与全特化不同,全特化不会写 template<typename T>,它是直接 template<>,然后指明具体的模板实参。

它与全特化最大的不同在于,全特化基本必写 template<>,而且定义的时候(如 s)是指明具体的类型,而不是一类类型(T*、T[])。


我们再举个例子:

cpp
template<typename T,typename T2>
const char* s = "?";

template<typename T2>
const char* s<int, T2> = "T == int";

std::cout << s<char, double> << '\n';       // ?
std::cout << s<int, double> << '\n';        // T == int
std::cout << s<int, std::string> << '\n';   // T == int

这种偏特化也是可以的,多个模板实参的情况下,对第一个模板实参为 int 的情况进行偏特化。

其他的各种形式无非都是我们提到的这两个示例的变种,类模板也不例外。

类模板偏特化

cpp
template<typename T,typename T2>
struct X{
     void f_T_T2();                 // 主模板,声明
};

template<typename T, typename T2>
void X<T, T2>::f_T_T2() {}          // 类外定义

template<typename T>
struct X<void,T>{
    void f_void_T();                // 偏特化,声明
};

template<typename T>
void X<void, T>::f_void_T() {}      // 类外定义

X<int, int> x;
x.f_T_T2();         // OK!
X<void, int> x2;
x2.f_void_T();      // OK!

稍微提一下类外的写法,不过其实通常不推荐写到类外,目前还好;很多情况涉及大量模板的时候,类内声明写到类外非常的麻烦。


我们再举一个偏特化类模板中的类模板,全特化和偏特化一起使用的示例:

cpp
template<typename T,std::size_t N>
struct X{
    template<typename T_,typename T2>
    struct Y{};
};

template<>
template<typename T2>
struct X<int, 10>::Y<int, T2> {     // 对 X<int,10> 的情况下的 Y<int> 进行偏特化
    void f()const{}
};

int main(){
    X<int, 10>::Y<int, void>y;
    y.f();                      // OK X<int,10> 和 Y<int> 
    X<int, 1>::Y<int, void>y2;
    y2.f();                     // Error! 主模板模板实参不对
    X<int, 10>::Y<void, int>y3;
    y3.f();                     // Error!成员函数模板模板实参不对
}

此示例无法在 gcc 通过编译,这是编译器 BUG 需要注意

语法形式是简单的,不做过多的介绍。

其实和全特化没啥区别。

实现 std::is_same_v

我们再写一个小示例,实现这个简单的 C++ 标准库设施。

cpp
template <class, class> // 主模板
inline constexpr bool is_same_v = false; 
template <class Ty>     // 偏特化
inline constexpr bool is_same_v<Ty, Ty> = true;

这是对变量模板的偏特化,逻辑也很简单,如果两个模板类型参数的类型是一样的,就匹配到下面的偏特化,那么初始化就是 true,不然就是 false

因为没有用到模板类型形参,所以我们只是写了 class 进行占位;这就和你声明函数的时候,如果形参没有用到,那么就不声明名字一样合理,比如 void f(int)

声明为 inline 的是因为 内联变量 (C++17 起)可以在被多个源文件包含的头文件中定义。也就是允许多次定义。

总结

我们在一开始的模板全特化花了很多时间讲解各种情况和细节,偏特化除了那个语法上,其他的各种形式并无区别,就不再介绍了。

本节我们给出了三个示例,也是最常见最基础的情况。我们要懂得变通,可能还有以此为基础的各种形式。值得注意的是,模板偏特化还可以和 SFINAE[1] 一起使用,这会在我们后续的章节进行讲解,不用着急。

如还有需求,查看 cppreference

最后强调一句:函数模板没有偏特化,只有类模板和变量模板可以


  1. 注:模板代换失败不是错误;在后续章节有详细讲解。 ↩︎