部分模板特化

来自cppreference.com
< cpp‎ | language

允许为给定的模板实参的类别定制类模板或变量模板 (C++14 起)

语法

template < 形参列表 > 类关键词 类头名 < 实参列表 > 声明 (1)
template < 形参列表 > 声明说明符序列 声明符 < 实参列表 > 初始化器(可选) (2) (C++14 起)

其中 类头名 标识先前声明的类模板,而 声明符 标识先前声明的变量模板 (C++14 起)。此声明必须与它所特化的主模板定义处于相同的命名空间中,或对于成员模板,处于相同的类作用域中。

例如,

template<class T1, class T2, int I>
class A {};             // 主模板
 
template<class T, int I>
class A<T, T*, I> {};   // #1:部分特化,其中 T2 是指向 T1 的指针
 
template<class T, class T2, int I>
class A<T*, T2, I> {};  // #2:部分特化,其中 T1 是指针
 
template<class T>
class A<int, T*, 5> {}; // #3:部分特化,其中 T1 是 int,I 是 5,T2 是指针
 
template<class X, class T, int I>
class A<X, T*, I> {};   // #4:部分特化,其中 T2 是指针

标准库中的部分特化的例子包括 std::unique_ptr,它对数组类型有部分特化。

实参列表

下列限制会应用到部分模板特化的 实参列表

1) 实参列表不能与非特化的实参列表相同(它必须有所特化):
template<class T1, class T2, int I> class B {};        // 主模板
template<class X, class Y, int N> class B<X, Y, N> {}; // 错误

此外,特化必须比主模板更特殊:

template<int N, typename T1, typename... Ts> struct B;
template<typename... Ts> struct B<0, Ts...> {}; // 错误:没有更特殊
(C++11 起)
2) 实参列表中不能出现默认实参
3) 如果有实参是包展开,那么它必须是列表中最后的实参
4) 非类型实参表达式可以使用模板形参,只要形参在不推导语境之外至少出现一次(注意当前只有 clang 支持此特性)
template<int I, int J> struct A {};
template<int I> struct A<I+5, I*2> {};  // 错误:I 不可推导
 
template<int I, int J, int K> struct B {};
template<int I> struct B<I, I*2, 2> {}; // OK:首个形参可以推导
5) 非类型模板实参不能特化类型【依赖该特化的形参】的模板形参:
template<class T, T t> struct C {}; // 主模板
template<class T> struct C<T, 1>;   // 错误:1 的类型是 T,它依赖形参 T
 
template<int X, int (*array_ptr)[X]> class B {}; // 主模板
int array[5];
template<int X> class B<X,&array> {}; // 错误:实参类型 &array 是 int(*)[X],依赖形参 X

名字查找

名字查找不会找到部分模板特化。只有在主模板被名字查找所找到时才会考虑它的部分特化。特别是使得主模板可见的 using 声明,也会使得部分特化可见:

namespace N
{
    template<class T1, class T2> class Z {}; // 主模板
}
using N::Z; // 指代主模板
 
namespace N
{
    template<class T> class Z<T, T*> {};     // 部分特化
}
Z<int, int*> z; // 名字查找找到 N::Z(主模板),然后使用带 T = int 的部分特化

偏序

当实例化类模板或变量模板 (C++14 起),且有部分特化可用时,编译器必须决定是继续使用主模板还是使用其中一个部分特化。

1) 如果只有一个特化与模板实参匹配,那么使用该特化
2) 如果有多于一个特化匹配,那么就用偏序规则确定哪个特化更加特殊。然后使用其中最特殊的特化(如果它不唯一,那么程序无法编译)
3) 如果没有匹配的特化,那么使用主模板
// 给定上面定义的模板 A
A<int, int, 1> a1;   // 没有匹配的特化,使用主模板
A<int, int*, 1> a2;  // 用部分特化 #1,(T=int, I=1)
A<int, char*, 5> a3; // 用部分特化 #3,(T=char)
A<int, char*, 1> a4; // 用部分特化 #4,(X=int, T=char, I=1)
A<int*, int*, 2> a5; // 错误:匹配 #2 (T=int, T2=int*, I=2) 和
                     //      匹配 #4 (X=int*, T=int, I=2) 都不比对方更特殊

非正式而言,“A 比 B 更特殊”的意思是“A 接受 B 所接受类型的子集”。

正式而言,为在部分特化之间建立“更特殊”关系,首先将其中每一个都转换成下列的虚设函数模板:

  • 第一个函数模板拥有与第一个部分特化相同的模板形参,且只有一个函数形参,它的类型是带有所有来自第一个部分特化的模板实参的类模板特化
  • 第二个函数模板拥有与第二个部分特化相同的模板形参,且只有一个函数形参,它的类型是带有所有来自第二个部分特化的模板实参的类模板特化。

然后,如同为函数模板重载所做的一样,对函数模板排行。

template<int I, int J, class T> struct X { }; // 主模板
template<int I, int J>          struct X<I, J, int>
{
    static const int s = 1;
}; // 部分特化 #1
// #1 的虚设函数模板是
// template<int I, int J> void f(X<I, J, int>); #A
 
template<int I>                 struct X<I, I, int>
{
    static const int s = 2;
}; // 部分特化 #2
// #2 的虚设函数模板是
// template<int I>        void f(X<I, I, int>); #B
 
int main()
{
    X<2, 2, int> x; // #1 和 #2 都匹配
// 函数模板的偏序处理:
// 从 #B 到 #A:从 void(X<U1, U1, int>) 到 void(X<I,J,int>):推导 OK
// 从 #A 到 #B:从 void(X<U1, U2, int>) 到 void(X<I,I,int>):推导失败
// #B 更特殊
// #2 是被实例化的特化
    std::cout << x.s << '\n'; // 打印 2
}

部分特化的成员

部分特化的成员的模板形参列表和模板实参列表必须与部分特化的形参列表和实参列表相匹配。

正如主模板的成员一样,它们只有在程序中使用时才需要定义。

部分特化的成员与主模板的成员无关。

部分特化的成员的显式(全)特化以与主模板的显式特化相同的方式声明。

template<class T, int I> // 主模板
struct A
{
    void f(); // 成员声明
};
 
template<class T, int I>
void A<T, I>::f() {}      // 主模板成员定义
 
// 部分特化
template<class T>
struct A<T, 2>
{
    void f();
    void g();
    void h();
};
 
// 部分特化的成员
template<class T>
void A<T, 2>::g() {}
 
// 部分特化的成员的显式(全)特化
template<>
void A<char, 2>::h() {}
 
int main()
{
    A<char, 0> a0;
    A<char, 2> a2;
    a0.f(); // OK,用主模板的成员定义
    a2.g(); // OK,用部分特化的成员定义
    a2.h(); // OK,用部分特化的成员的全特化定义
    a2.f(); // 错误:部分特化 A<T, 2> 中没有 f() 的定义(没有使用主模板)
}

如果主模板是另一个类模板的成员,那么它的部分特化也是外围类模板的成员。如果外围类模板被实例化,那么每个成员部分特化的声明也会被实例化(模板的所有其他成员的声明(但非其定义)也是以相同方式实例化的)

如果针对外围类模板的给定(隐式)特化进行主成员模板的显式(全)特化,那么对外围类模板的这个特化忽略该成员模板的部分特化。

如果针对外围类模板的给定(隐式)特化进行成员模板的某个部分特化的显式特化,那么对外围类模板的这个特化仍然考虑主成员模板及它的其他部分特化。

template<class T> struct A // 外围类模板
{
    template<class T2>
    struct B {};      // 主成员模板
    template<class T2>
    struct B<T2*> {}; // 成员模板的部分特化
};
 
template<>
template<class T2>
struct A<short>::B {}; // 主成员模板的全特化(忽略部分特化)
 
A<char>::B<int*> abcip;  // 使用部分特化 T2=int
A<short>::B<int*> absip; // 使用主模板的全特化(忽略部分特化)
A<char>::B<int> abci;    // 使用主模板

缺陷报告

下列更改行为的缺陷报告追溯地应用于以前出版的 C++ 标准。

缺陷报告 应用于 出版时的行为 正确行为
CWG 1315 C++98 不能在除标识表达式之外的非类型模板实参中使用模板形参 只要能推导,表达式就 OK
CWG 1495 C++11 涉及形参包时规范不明确 特化必须更为特殊