模板的全特化与偏特化 | C & C++

模板机制为 C++ 提供了泛型编程的方式,在减少代码冗余的同时仍然可以提供类型安全。特化必须在同一命名空间下进行,可以特化类模板也可以特化函数模板,但类模板可以偏特化和全特化,而函数模板只能全特化。C++ 的模板机制被证明是图灵完备的,即可以通过模板元编程(template meta programming)的方式在编译期做任何计算。

模板的声明

类模板和函数模板的声明方式是一样的,在类定义/模板定义之前声明模板参数列表。例如:

1
2
3
4
5
6
7
8
9
10
11
12
// 类模板
template<class T1, class T2>
class A {
T1 data1;
T2 data2;
};

// 函数模板
template<class T>
T max(const T lhs, const T rhs){
return lhs > rhs ? lhs : rhs;
}

全特化

通过全特化一个模板,可以对一个特定参数集合自定义当前模板,类模板和函数模板都可以全特化。全特化的模板参数列表应当是空的,并且应当给出“模板实参”列表:

1
2
3
4
5
6
7
8
9
10
11
12
// 全特化模板类
template <>
class A<int, double> {
int data1;
double data2;
};

// 全特化模板函数
template <>
int max(const int lhs, const int rhs) {
return lhs > rhs ? lhs : rhs;
}

类模板的全特化时在类名后给出了“模板实参”,但函数模板的函数名后没有给出“模板实参”。这是因为编译器根据 int max(const int, const int) 的函数签名可以推导出来它是 T max(const T, const T) 的特化。

注意:使用时需要将全特化模板函数的实现放在使用该函数之前,否则会报 specialization xxx after instantiation 的错误。或者在使用之前进行全特化声明也可以。

特化的歧义

上述函数模板不需指定”模板实参”是因为编译器可以通过函数签名来推导,但有时这一过程是有歧义的:

1
2
3
4
5
6
7
8
9
template <typename T>
void f() {
T d;
}

template <>
void f() {
int d;
}

此时编译器不知道 f() 是从 f<T>() 特化来的,编译时会提示错误 error: no function template matches function template specialization 'f'。这时我们便需要显式指定“模板实参”:

1
2
3
4
5
6
7
8
9
template <typename T>
void f() {
T d;
}

template <>
void f<int>() {
int d;
}

偏特化(部分特化)

偏特化是全特化的一种特殊情况,通用性要低于全特化,可以理解为部分特化。类似于全特化,偏特化也是为了给自定义一个参数集合的模板,但偏特化后的模板需要进一步的实例化才能形成确定的签名。函数模板不允许偏特化。偏特化也是以 template 来声明的,需要给出剩余的“模板形参”和必要的“模板实参”:

1
2
template <class T2>
class A<int, T2> {};

函数模板是不允许偏特化的,下面的声明会编译错:

1
2
3
4
5
6
7
// 全特化模板函数
template <class T1, class T2>
void f() {}

// 偏特化模板函数(编译错误)
template <class T2>
void f<int, T2>() {}

但函数允许重载,声明另一个函数模板即可替代偏特化的需要:

1
2
template <class T2>
void f() {} // 注意:这里没有"模板实参"

多数情况下函数模板重载就可以完成函数偏特化的需要,一个例外便是 std 命名空间。std 是一个特殊的命名空间,用户可以特化其中的模板,但不允许添加模板(其实任何内容都是禁止添加的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
template <class T, bool USESPE>
class Slot {
public:
void Show();
};

template <class T>
class Slot<T, true> {
public:
void Show();
};

template <class T>
void Slot<T, true>::Show() {
std::cout << "true" << std::endl;
}

template <class T>
class Slot<T, false> {
public:
void Show();
};

template <class T>
void Slot<T, false>::Show() {
std::cout << "false" << std::endl;
}

int main() {
Slot<int, true> a;
a.Show();

Slot<int, false> b;
b.Show();
}

至于为什么模板函数不能偏特化,这仅仅是 C++ 标准不允许。可能是因为使用重载也可以达到这个目的。

函数模板的特化

1
2
3
4
5
6
7
8
9
10
11
template<class T>
void f(T); // (a),一个基模板

template<class T>
void f(T*); // (b),另一个基模板,对 (a) 进行了重载

template<>
void f<>(int*); // (c)

int *p;
f(p); // calls (c)
1
2
3
4
5
6
7
8
9
10
11
template<class T>
void f(T); // (a),一个基模板

template<>
void f<>(int*); // (c)

template<class T>
void f(T*); // (b),另一个基模板,对 (a) 进行了重载

int *p;
f(p); // calls (b)

可以看到两个例子里面的特化函数是一模一样的,只是相应的基模板不同。出现这种现象是因为在编译器解析重载的时候只考虑基模板(当然如果有非模板函数更合适,则选择该非模板函数)。当基模板被选定之后,编译器才会去查看有没有合适的特化版本。

函数模板的特化函数不参与重载的原因很简单:如果你为函数模板写了特化函数,那么你就希望这个特化函数被调用,而如果其他人为另一个函数模板写了个特化函数,其他人也希望这个特化函数被调用,那么结果可能会不如你所愿,所以 C++ 标准禁止了特化函数参与重载。

参考

C++模板的偏特化与全特化

该不该特化函数模板?

模板的全特化与偏特化 | C & C++

http://www.zh0ngtian.tech/posts/d958ea1d.html

作者

zhongtian

发布于

2020-08-31

更新于

2023-12-16

许可协议

评论