前言

模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。

模板

类模板

适用于描述通用但类型安全的数据结构,如下:

#include <iostream>

// class template
template <typename T>
class Compare {
public:
bool equal(T a, T b);
};

template <typename T>
bool Compare<T>::equal(T a, T b) {
return a == b;
}

int main() {
Compare<int> C;
std::cout << C.equal(1, 2) << std::endl;
return 0;
}

函数模板

#include <iostream>

// function template
template <typename T1, typename T2>
bool Compare(T1 a, T2 b) {
return a == b;
}

int main() {
std::cout << Compare(1, 2) << std::endl;
return 0;
}

注意:类的成员函数模板不能为虚函数。因为每个包含虚函数的类具有一个virtual table,包含该类的所有虚函数的地址,因此virtual table的大小是确定的。模板只有被使用时才会被实例化,将其声明为虚函数会使virtual table的大小不确定。所以,成员函数模板不能为虚函数。

特化

所谓特化就是实现模板的一个特例,即将全部或部分模板类型参数固定,用于解决特定类型下的实现与通用模板实现不一样的问题。将模板类型参数全部固定的称为全特化(total/fully template specialization),而只固定部分模板类型参数的称为偏特化(partially specialization)。

全特化

类模板和函数模块都支持全特化。全特化的特征之一就是template <>,即我们不再需要为其提供任何不确定的模板参数。

类模板的全特化:

#include <iostream>

// class template
template <typename T>
class Compare {
public:
bool equal(T a, T b);
};

template <typename T>
bool Compare<T>::equal(T a, T b) {
return a == b;
}

// class template total specialization
template <>
class Compare<float> {
public:
bool equal(float a, float b);
};

bool Compare<float>::equal(float a, float b) {
return std::abs(a - b) < 10e-3;
}

int main() {
Compare<int> C;
std::cout << C.equal(1, 2) << std::endl;
Compare<float> Cf;
std::cout << Cf.equal(1.00001, 1.00002) << std::endl;
return 0;
}

函数模板的全特化:

#include <iostream>

// function template
template <typename T1, typename T2>
bool Compare(T1 a, T2 b) {
std::cout << "uniform template" << std::endl;
return a == b;
}

// function template total specialization
template <>
bool Compare(const char* a, const char* b) {
std::cout << "total template specialization" << std::endl;
return strcmp(a, b) == 0;
}

int main() {
std::cout << Compare(1, 2) << std::endl;
std::cout << Compare("abc", "abc") << std::endl;
return 0;
}

偏特化

只有类模板可以偏特化,而函数模板没有偏特化。至于为什么?因为C++规定不行。再深究原因的话,我个人不严谨的推测是:函数本身是可以重载的,重载和(全)特化在某些情况下就已经纠缠不清了(这点后面会详细阐述)。再把偏特化搅和进来,两者的用途和区分规则就更难定义清楚了。所以干脆禁止吧。

#include <iostream>

template <typename T1, typename T2>
class Test {
public:
Test(T1 a, T2 b) : a_(a), b_(b) {
std::cout << "class template" << std::endl;
}

private:
T1 a_;
T2 b_;
};

template <>
class Test<int, int> {
public:
Test(int a, int b) : a_(a), b_(b) {
std::cout << "total specialization" << std::endl;
}

private:
int a_;
int b_;
};

template <typename T>
class Test<int, T> {
public:
Test(int a, T b) : a_(a), b_(b) {
std::cout << "partially specialization" << std::endl;
}

private:
int a_;
T b_;
};

int main() {
Test<double, double> t1(1.01, 1.01);
Test<int, int> t2(1, 1);
Test<int, bool> t3(1, false);
return 0;
}

需要清楚的是,并不是只有两个(多个)模板类型参数确定其中一个(多个)的情况才叫偏特化,只要确定了任意一个模板类型参数的部分信息就是偏特化。一个例子如下:

#include <iostream>

template <typename T>
class X {
public:
X(T a) : a_(a) {
std::cout << "class Template" << std::endl;
}

private:
T a_;
};

template <typename T>
class X<T*> {
public:
X(T* a) : a_(a) {
std::cout << "partially specialization" << std::endl;
}

private:
T* a_;
};

int main() {
X<int>(1);
X<char*>((char*)"hello world");
}

特化与重载

普通的C++类不会重载,因此模板类也不会重载;普通的C++函数会重载,因此模板函数也会重载。没有特化的模板称为基础模板(base templates)

// A class template
template<typename T> class X { /*...*/ }; // (a)

// A function template with two overloads
template<typename T> void f( T ); // (b)
template<typename T> void f( int, T, double ); // (c)

进一步,基础模板可以被特化。类模板和函数模板在此进一步分化,类模板可以全特化和偏特化;而函数模板只可以进行全特化。但由于函数模板可以重载,我们可以通过重载获得与偏特化近乎一致的效果。

// A partial specialization of (a) for pointer types 
template<typename T> class X<T*> { /*...*/ };

// A full specialization of (a) for int
template<> class X<int> { /*...*/ };

// A separate base template that overloads (b) and (c)
// -- NOT a partial specialization of (b), because
// there's no such thing as a partial specialization
// of a function template!
template<typename T> void f( T* ); // (d)

// A full specialization of (b) for int
template<> void f<int>( int ); // (e)

// A plain old function that happens to overload with
// (b), (c), and (d) -- but not (e), which we'll
// discuss in a moment
void f( double ); // (f)

那么,对于函数模板的重载和特化,调用时的匹配规则如下:

  • 首先,非模板函数是第一公民。将优先选择与参数匹配的非模板函数,而不是同样能够匹配的模板函数;
  • 其次,当没有合适的一等公民时,基础模板将作为二等公民被选择。当多个基础模板的重载存在时,选择参数类型匹配最好的基础模板。
  • 最后,如果该基础模板还有满足参数匹配要求的特化,则选择该特化;若没有满足的,则选择基础模板本身。
bool b; 
int i;
double d;

f( b ); // calls (b) with T = bool
f( i, 42, d ); // calls (c) with T = int
f( &i ); // calls (d) with T = int
f( i ); // calls (e)
f( d ); // calls (f)

需要注意的是,特化的函数模板的选择是在确定了基础模板之后才开始执行的。而且特化函数没有重载。怎么理解呢?看下面两个例子:

template<class T> // (a) a base template 
void f( T );

template<class T> // (b) a second base template, overloads (a)
void f( T* ); // (function templates can't be partially
// specialized; they overload instead)

template<> // (c) explicit specialization of (b)
void f<>(int*);

// ...

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

在上例中,(c)(b)的特化(若没有(b)(c)将会成为(a)的特化),这符合我们的预期,因为(b)(a)更“接近”我们的特化。函数调用时的选择(c)也符合我们的预期。那么再往下看:

template<class T> // (a) same old base template as before 
void f( T );

template<> // (c) explicit specialization, this time of (a)
void f<>(int*);

template<class T> // (b) a second base template, overloads (a)
void f( T* );

// ...

int *p;
f( p ); // calls (b)! overload resolution ignores
// specializations and operates on the base
// function templates only

仅仅是交换了一下声明的顺序,结果便发生了变化。理解的关键就是特化不会重载。即(b)不是c的重载,而是a的重载。而基础模板的重载是二等公民(此处没有一等公民),是要在特化的选择之前就确定的。也就是说,编译器先在(a)(b)之间做选择,发现(b)更合适,就确定了(b)为待选,而(b)没有特化(由于声明顺序的改变,(c)成为了(a)的特化),就直接用基础模板了。

参考

[1] C++ 模板 全特化与偏特化

[2] Why Not Specialize Function Templates?