blueyi's notes

Follow Excellence,Success will chase you!

0%

C++学习笔记之模板与泛型编程

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处是OOP能处理类型在程序运行之前都未知的情况(即通过多态的动态绑定);而泛型编程中,在编译时就能确定类型,只是可以应用于不同的类型,例如vector可以放int,也可以放string等。
既可以定义函数模板也可以定义类模板。

函数模板

1.一个函数模板就相当于一个公式,用来生成针对特定类型的函数版本。模板定义以关键字template开始,后跟一个模板参数列表(template parameter list),多个模板参数需要由逗号分割,使用尖括号包围起来,参数列表不能为空,模板参数表示在类或函数中用到的类型或值,当使用模板时,隐式或显示地指定模板实参(template argument),将其绑定到模板参数上。eg:

1
2
3
4
5
//声明一个函数模板,该函数接受一个参数类型为T的指针,返回值类型为T
template <typename T> T foo(T* p)
{
return *p;
}

2.当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新”实例“。

3.模板的类型参数T可以用来指定返回类型和函数的参数类型,以及在函数体内用于变量声明或类型转换。

4.类型参数前必须使用关键字class或typename。这个两个关键字的含义完全相同。eg:

1
template <typename T, class U> calc (const T&, const U&);

5.除了定义类型参数外,还可以使用模板定义非类型参数(nontype parameter),一个非类型实参是一个值而非一个类型,非类型实参使用一个特定的类型名而非关键字class或typename来指定。传递给非类型实参的值必须是常量表达式。eg:

1
2
3
4
5
6
7
8
9
10
11
12
//N和M可以是任意两个能被编译器推断出类型的常量表达式
//由于不能拷贝数组,所以将类型定义为const的引用
template<unsigned N, unsigned M> //该行与下一行不强制换行
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
//调用
//编译器将自动推断出所需要的N和M是字符串的大小+1
compare("hi", "mom");
//编译器实例化的版本如下
int compare(const char (&p1)[3], const char (&p2)[3])

6.一个非类型实参可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。但必须是一个常量表达式。

7.函数模板可以声明为inline或constexpr的,这两个关键字必须放在模板参数列表之后,返回类型之前。eg:

1
template <typename T> inline T min(const T&, const T&);

8.模板程序应该尽量减少对实参类型的要求,例如可以考虑使用functional头文件中支持比较指针的比较模板。

9.函数模板和类模板成员函数的定义通常放在头文件中。当编译器遇到一个模板定义时,它并不生成代码,只有当实例化出一个特定版本时,编译器才会生成模板。

10.保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作是调用者的责任。

类模板

1.类模板的定义与使用和函数模板类似,注意在类模板的继承关系中,子类模板不能直接访问父类的成员,需要使用this指针,或者间接访问运算符。所以定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template <typename T> class Base_temp {
public:
Base_temp() = default;
Base_temp(T i, T j) : x(i), y(j) {}

private:
T x;
T y;
};

template <typename T> class Driv : public Base_temp<T> {
public:
Driv() = default;
Driv(T i, T j, T k) : Base_temp<T>(i, j), z(k) {}
//
void modify(std::ostream &os) { os >> this->i >> Base_temp<T>::j >> k; }

private:
T z;
};

2.编译器不能为类模板推断类型,所是使用时需要通过尖括号提供类型信息,例如Driv<int> driv(1, 2, 3);

3.定义在类模板之外的成员函数必须以template开始,后接类模板参数列表,并且类名后面也要有尖括号提供类型信息,形如:

1
2
3
4
5
template <typename T>
ret-type class_name<T>::member-name(parm-list)
{
//...
}

如:

1
2
3
4
5
6
template <typename T>
void Blob<T>::check(size_type i, const std::string &msg) const
{
if (i >= data->size())
throw std::out_of_range(msg);
}

4.默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

5.在类模板的作用域内,可以直接使用模板名而不必指定模板实参。eg:

1
2
3
4
5
6
7
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int)
{
BlobPtr ret = *this; //直接使用模板名,而无需提供模板实参
++*this;
return ret;
}

模板与友元

1.当一个类包含友元声明时,类与友元各自是否是模板是相互无关的。如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
为了引用(类或函数)模板的一个特定实例,必须首先声明模板自身。如:

1
2
3
4
5
6
7
8
9
10
11
//前置声明,在Blob中声明友元所需要的
template <typename> class BlobPtr;
template <typename> class Blob;
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
//类及其友元声明
template <typename T> class Blob {
//每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>(const Blob<T>&, const Blob<T>&);
};

注意:每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符,即一对一的友好关系

2.一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//前置声明,后面将用类C实例化的pal声明为C的一个友元
template <typename T> class Pal;
class C { //C是一个普通的非模板类
friend class Pal<C>; //用类C实例化的Pal是C的一个友元
// Pal2的所有实例都是C的友元,这种情况无法前置声明
template <typename T> friend class Pal2;
};
template <typename T> class C2 { //C2本身是一个类模板
//C2的每个实例将相同实例化的Pal声明为友元
friend class Pal<T>; //Pal的模板声明必须在作用域之内
//Pal2的所有实例都是C2的每个实例的友元,不需要前置声明
template <typename X> friend class Pal2;
//Pal3是一个非模板类,它是C2所有实例的友元
friend class Pal3; //不需要Pal3的前置声明
};

为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
3.令模板自己的类型参数成为友元(C++11):

1
2
3
template <typename Type> class Bar {
friend Type; //将访问权限授予用来实例化Bar的类型
};

如对于某个类型名Foo,Foo将成为Bar的友元

4.模板类型别名
可以使用typedef方式来引用补货化的类:typedef Bolb<string> StrBlob;,但由于模板不是类型,所以不能定义一个typedef引用一个模板,即无法定义一个typedef引用Blob。但C++11中可以使用using为类模板定义一个类型别名:

1
2
template<typename T> using twin = pair<T, T>;
twin<string> authors; //authors是一个pair<string, string>

当定义一个模板类型别名时,可以固定一个或多个模板参数:

1
2
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; //books是一个pair<T, unsigned>

这种情况下必须指定pair的first成员,但不能指定pair的second成员

5.类模板可以定义static成员,当定义类模板的static成员时,该类模板实例化的每个实例类型都有一个独有的static对象,该实例类型的所有对象共有static成员。static成员的定义与定义模板的成员函数类似,定义的开始部分模板参数列表:

1
2
template <typename T>
size_t Foo<T>::ctr = 0; //定义并初始化ctr

模板参数

1.类似函数参数的名字,模板参数的名字T并没有任何内存意义,可以使用任何合法的名字。

2.模板参数遵循普通的作用域规则,例如同样存在内层同名类型隐藏外层同名类型的情况

3.模板声明必须包含模板参数,与函数参数类似的是声明中的模板参数的名字不必与定义中相同:

1
2
3
//声明但不定义compare和Blob
template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;

一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前.
4.默认情况下,C++语言假定通过作用域运算符访问的名字不是类型,所以如果希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型,通过关键字typename来实现:

1
2
3
4
5
6
7
8
template <typename T>  //类型T含有成员value_type
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type type();
}

当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class

5.类似可以为函数参数提供默认实参一样,也可以提供默认模板实参,C++11中可以为函数和类模板提供默认实参,早期的C++只允许为类模板提供默认实参。eg:

1
2
3
4
5
6
7
8
//compare有一个默认默认实参less<T>和一个默认函数实参F()
template <typename T, typename F = less<T>>
int compare(const T &v1, const T &v2, F f = F()) //默认函数实参指出f将绑定到一个函数对象
{
if (f(v1, v2)) return -1;
if (v2, v1) return 1;
return 0;
}

调用方法为:

1
2
bool i = compare(0, 42);  //使用less, i 为-1
bool j = compare(item1, item2, compareIsbn); //item1和item2为自定义类型,compareIsbn为函数对象

6.类模板的默认模板实参:无论何时使用类模板,都必须在模板名之后接上尖括号,尖括号指出类必须从一个模板实例化而来,特别是如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:

1
2
3
4
5
6
7
8
template <class T = int> class Numbers {  //T默认为int
public:
Numbers(T v = 0): va(v) {}
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; //使用默认类型int

成员模板

1.一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数,这种成员函数被称为成员模板(member template)。成员模板不能是虚函数。

普通(非模板)类的成员模板

正常定义成员模板即可,如下定义一个类DebugDelete来封装delete操作,使delete操作之前打印一打消息。

1
2
3
4
5
6
7
8
9
10
11
12
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) {}
//与任何函数模板相同,T的类型由编译器推断
template <typename T> void operator()(T *p) const
{
os << "deleting ptr" << std::endl;
delete p;
}
private:
std::ostream &os;
}

用法:

1
2
3
4
5
double *p = new double;
DebugDelete d;
d(p); //调用DebugDelete::operator()(double*),释放p
int *ip = new int;
DebugDelete()(ip); //在一个临时对象上调用函数对象

类模板的成员模板

对于类模板,类和成员各自有自己的、独立的模板参数。定义方法如下:

1
2
3
template <typename T> class Blob {  //定义模板类
template <typename It> Blob(It b, It e); //声明类模板的成员模板
}

在模板类外定义成员模板时,必须同时提供类模板和成员模板的参数列表:

1
2
3
template <typename T>  //类的类型参数
template <typename It> //构造函数的类型参数
Blob<T>::Blob(It b, It e):data(std::make_shared<std::vector<T>>(b, e)) {}

实例化成员模板时,必须同时提供类和函数模板的实参,如:

1
2
int ia[] = {0, 1, 2, 3, 4};
Blob<int> a1(begin(ia), end(ia));

使用extern控制实例化

由于模板被使用时才会进行实例化,意味着相同的实例可以出现在多个对象文件中,当两个或多个独立编译的源文件使用了相同的模板时,并提供了相同的模板参数时,每个文件都会有一个该模板的实例。C++11中可以通过extern来显式实例化(explicit instantiation)来避免这种开销。显示实例化形如:

1
2
extern template declaration;  //实例化声明
template declaration; //实例化定义

declaration是一个类或函数声明,其中所有模板参数已被替换为模板实参,例如:

1
2
3
//实例化声明与定义
extern template class Blob<string>; //声明
template int compare(const int&, const int&); //定义

当编译器遇到extern模板声明时,它不会在本文件中生成实例化代码,将一个实例化声明为extern就表示承诺在程序其他位置有文件实例化的一个非extern声明(定义)。对于一个给定的实例化版本,可能有多个extern声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern声明必须出现在任何使用些实例版本的代码之前。

模板实参推断

指定显式模板实参

可以为函数模板指定显式模板实参,以控制函数的返回类型与实例化
如下面定义的sum函数模板,接受两个不同类型的参数,并返回一个不同类型的结果:

1
2
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3); //由于参数列表中没有T1,所以编译器无法推断T1的类型,必须显式提供

用法:

1
auto val3 = sum<double>(42, 33.2f);  //double sum(int, float)

显式模板实参按由左至右的顺序与对应的模板参数匹配,只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。如果sum函数模板中这样写T3 sum(T2, T1),其他不变,则此时必须同时指定3个实参:

1
auto val2 = sum<double, int, float>(42, 33.2f);

对于指定了模板类型实参的函数实参会发生正常的类型转换。

尾置返回类型与类型转换

通过尾置返回类型返回引用
由于尾置返回出现在参数列表之后,它可以使用函数的参数,尾置返回允许我们在参数列表之后声明返回类型,并可以使用decltype来推断返回类型。例如编写一个函数,接受表示序列的一对迭代器,并返回序列中一个元素的引用,由于此时并不知道引用的类型,引用的类型只有在确定迭代器之后才能知道,这种情况就可以使用尾置返回,如:

1
2
3
4
5
6
template <typename T>
auto fcn(It beg, It end) -> decltype(*beg)
{
//处理序列
return *beg; //返回序列中一个元素的引用
}

通过标准库的类型转换模板返回元素的值
类似上面的情况,假如要返回元素的值,由于上述情况下迭代器的操作都不会生成元素,只能生成元素的引用,所以无法返回元素的值。可以使用标准库的类型转换模板来获得元素的类型,然后再返回一个值。该模板定义在头文件type_traits中。可以使用其中的remove_reference来获得元素的类型,该模板有一个模板类型参数和一个名为type的类型成员,如remove_reference<int&>,则type成员将是int。如上例返回值而非引用,改写为:

1
2
3
4
5
6
template <typename T>
auto fcn2(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type
{
return *beg; //返回序列中一个元素的拷贝
}

函数指针与实参推断

当用一个函数模板初始化一个函数指名或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。如:

1
2
3
template <typename T> int compare(const T&, const T&);
//pf1指向实例int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;

函数模板实参推断与引用

1.对于带有引用类型的模板类型参数推断,首先需要明白两点:编译器会应用正常的引用绑定规则(即&绑定到左值,const &即可绑定到左值,又可绑定到右值,&&绑定到右值);const是底层的,不是顶层的(即const是类型的一部分)。
从左值引用函数参数推断类型
当函数参数是模板类型参数的一个普通(左值)引用时(即,形如T&),绑定规则告诉我们,只能传递给它一个左值(如,一个变量或一个返回引用类型的表达式)。实参可以是const类型,也可以不是。如果实参是const类型,则T将被推断为const类型。如:
f1为非const的普通引用

1
2
3
4
template <typename T> void f1(T&); //实参必须是一个左值
f1(i); //i是一个int,则T为int
f1(ci); //ci是一个const int;则T为const int
f1(5); //错误,传递给&的必须是左值

f2为const的普通引用

1
2
3
4
template <typename T> void f2(const T&); //实参是一个左值或右值
f2(i); //i是一个int,则T为int
f2(ci); //ci是一个const int;则T为int
f2(5); //正确,T是一个int

从右值引用函数参数推断类型
右值引用的类型推断类似左值引用。如:

1
2
template <typename T> void f3(T&&);
f3(42); //实参是一个int类型的右值;模板参数T是int

将右值引用绑定到左值的例外
通常不能将一个右值引用绑定到一个左值上,但C++在正常的绑定规则下定义了两个例外规则:一个是当将一个左值(如变量i)传递给函数的右值引用参数,且此右值引用指向模板类型参数(如T&&)时,编译器推断模板类型参数为实参的左值引用类型,如f3(i)中T类型为int&,而非int。另一个是 引用折叠产生的例外使我们可以在模板类型参数上,将一个右值引用绑定到一个左值上,通常不能创建引用的引用,但可以通过类型别名或模板类型参数间接定义引用的引用。当间接创建一个引用的引用时,这些引用将发生“折叠”,除了右值引用的右值引用会折叠成右值引用之外,其他所有情况下,引用的引用都会折叠成一个普通的左值引用。如:

  • X& &、X& &&和X&& &都折叠成类型X&
  • 类型X&& &&折叠成X&&
    如上面定义的变量及f3:
    1
    2
    f3(i);   //T将为int&
    f3(ci); //T是一个const int&
    这种右值引用的例外将导致:
  • 如果一个函数参数是指向模板参数类型的右值引用(如,T&&),则可以传递给它任意类型的实参。如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通的左值引用(T&)。*
    右值引用通常用于两种情况:模板转发其实参或模板被重载

C++11中 std::move的工作方式表明可以使用static_cast显式地将一个左值转换为一个右值引用,通常情况下static_cast只能用于合法的类型转换,所以这也是一条针对右值引用的特许规则。使用方法如std::move的定义:

1
2
3
4
5
6
//在返回类型和类型转换中需要用typename指明成员是一个类型而不是成员变量名称
template <typename T>
typename remove_reference<T>::type&& move(T&& t) //前面的typename是告诉编译器type是个类型而不是成员变量名
{
return static_cast<typename remove_reference<T>::type&&>(t); //typename同样是告诉编译器type是一个类型而不是成员变量名
}

当通过模板使用模板类中的类型时,需要使用typename指定此时引用的是个类型,而不是成员变量名
例如:

1
2
std::vector<T> vec;
typename std::vector<T>::iterator it = vec.begin(); //此时需要通过typename指定iterator是个类型,而不是一个变量名

参数转发

1.参数转发即指将函数的一个或多个实参连同类型等所有性质保持不变地转发给其他函数,包括实参类型是否是const,以及实参是左值还是右值。
如果一个函数参数是指向模板类型参数的右值引用(如T&&),它对应的实参的const属性和左传/右值属性将得到保持

2.C++11中可以使用std::forward保持类型信息。std::forward类似move,定义在头文件utility中,与move不同的时,forward必须通过显式模板实参来调用,forward返回该显式实参类型的右值引用,即forward的返回类型是T&&。
当用于一个指向模板参数类型的右值引用函数参数(T&&)时,forward会保持实参类型的所有细节
如下面的函数flip将其参数t1、t2传递给函数f:

1
2
3
4
5
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}

调用:flip(g, i, 42);,i将以int&类型传递给g,42将以int&&类型传递给g

重载与模板

函数模板可以被另一个模板或一个普通模板函数重载。
涉及有函数模板的重载,在函数匹配时,如果即有非模板函数又有模板函数时,会优先选择非模板函数。当有多个函数模板可选择时,会在类型转换的情况下,优先选择更特例化的模板。
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数而实例化一个并非你需要的版本

可变参数模板

1.一个可变参数模板就是一个可以接受可变数目参数的模板函数或模板类,当然这里我们只会用到模板函数。可变数目的参数被称为参数包,包括模板参数包(由0个多或多个类型组成)和函数参数包(由0个或多个函数参数组成)。可变参数的函数模板声明通常形如:

1
2
3
//Args是一个模板参数包,rest是一个函数参数包
template <typename T, typename ... Args>
void func(const T &t, const Args& ... rest); //扩展Args

用一个省略号来指出一个模板参数或函数参数表示一个包。在一个模板参数列表中,class…或typename…指出接下来的参数表示0个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。
也就是typename 省略号(...) 模板参数包名字来定义一个模板参数包,扩展模式 模板包名字 省略号(...) 函数参数包名字来将模板包扩展成相应模式下的函数参数包,同样当将函数参数包名字后跟省略号时,即可扩展函数参数包。当编译器在对模板特例化时,会自动按照提供给参数包的模式进行扩展包,扩展之后的参数相当于一个以逗号分隔的参数列表,如上例中const Arg&会应用到模板参数包Args中的每一个元素,也就是最终rest中包含的所有元素都将是const的引用,类型由编译器根据传递的参数进行自动推断。

2.与可变参数模板对应的还有一个sizeof…运算符,用于返回参数包中的参数个数,当然可以用它来配合递归函数访问各参数。
注意可变参数模板的使用不需要再指定类型,具体类型由编译器根据传递给函数的实参进行推断。
3.当需要在函数内部访问各参数列表中的参数时,直接对函数参数包进行扩展即可,通常是采用递归的方式访问各参数,为了终止递归就需要定义一个重载的函数用于终止递归
下面还是以一个计算和的例子分析:

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
36
37
38
39
40
41
42
43
#include <iostream>

template <typename T, typename... Args>
void foo(const T &t, const Args& ... args)
{
std::cout << "sizeof...(Args): " << sizeof...(Args) << std::endl; //输出模板参数Args的个数
std::cout << "sizeof...(args): " << sizeof...(args) << std::endl; //输出函数参数args的个数
}

template <typename T>
std::ostream& print(std::ostream &os, const T &t) //用于结束递归,当参数个数为2个时调用
{
return os << t << std::endl;;
}

template <typename T, typename ... Args>
std::ostream& print(std::ostream &os, const T &t, Args ... rest) //当参数个数大于2个时调用
{
os << t << ", ";
return print(os, rest...);
}

template <typename T>
T sum_template(const T &t) //声明一个重载的固定参数数目的函数,用于结束递归
{
return t;
}

template <typename T, typename... Args>
T sum_template(const T &t, const Args&... args) //该函数通过调用仅含一个参数的重载函数来结束递归
{
return sum_template(args...) + t; //展开函数参数包args中的实参
}


int main(void)
{
foo('a', 2, 3, "ssl");
print(std::cout, 'a', 2, 3, "ssl");
std::cout << sum_template(1, 2, 3, 4, 5) << std::endl;
std::cout << sum_template(1.1, 2.5, 3, 4.4, 5) << std::endl;
return 0;
}

输出结果为:

1
2
3
4
5
sizeof...(Args): 3
sizeof...(args): 3
a, 2, 3, ssl
15
15.6

根据后面的求和输出结果可见,对于将double与int混合计算时结果是错误的,因为计算过程中发生了double到int的转换,导致精度损失,所以对于这种情况要注意类型转换问题。

包扩展

1.对于一个参数包,除了获取其大小外,唯一对它能做的就是扩展(expand),当扩展一个包时,需要提供用于每个扩展元素的模式(pattern),扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边放一个省略号(…)来触发扩展操作。
2.包扩展除了上面用到的将函数参数包中的每个元素应用模式const &这类属性之外,还支持更复杂的扩展,例如对每个元素调用某个函数:

1
2
3
4
5
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&... rest)
{
return print(os, debug_rep(rest)...);
}

对rest参数包中的每个元素调用函数debug_rep

转发参数包
C++11中可以使用forward转发参数包。如std::forward<Args>(args)...转发类型为Args的参数包args中的类型参数。

模板特例化

1.一个模板特例化(template specialization)版本就模板的一个独立的定义,在其中一个或多个模板参数被指定为特定的类型

函数模板特例化

2.定义函数模板的特例化版本时,必须为原模板中的每个模板参数都提供实参。为了指定我们正在实例化一个模板,应使用template后跟一个空尖括号对(<>),空尖括号指出我们将为原模板的所有模板参数提供实参:

1
2
3
4
5
6
7
8
//原模板
template <typename T> int compare(const T&, const T&);
//compare的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}

特例化的本质是实例化一个模板,而非重载它,因此特例化不影响函数匹配
为了特例化一个模板,原模板的声明必须在作用域中,所以模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。

类模板特例化

1.类模板特例化时,必须在原模板定义所在命名空间中特例化它。当特例化类模板的所有模板参数时,定义方式与函数模板特例化类似。
2.类模板支持部分特例化。一个类模板的部分特例化(partial specialization)本身是一个模板,所以使用时必须提供那个未指定的模板参数。如:

1
2
3
4
5
6
7
8
9
10
11
//原始的通常版本
template <class T> strcut remove_reference {
typedef T type;
};
//部分特例化版本,将用于左值引用和右值引用
template <class T> strcut remove_reference<T&> { //左值引用
typedef T type;
};
template <class T> strcut remove_reference<T&&> { //右值引用
typedef T type;
};

调用:

1
2
3
remove_reference<(decltype(42))>::type a;  //使用原始版本
remove_reference<(decltype(i))>::type b; //i为int类型变量,使用第一个部分特例化版本
remove_reference<(decltype(std::move(i)))>::type c; //i为int类型变量,使用第二个部分特例化版本

也可以只 特例化成员而不是类,如下Foo是一个模板,包含一个成员Bar,后面将只特例化该成员:

1
2
3
4
5
6
7
8
9
10
11
12
template <typename T> struct Foo {
Foo(const T &t = T()) : mem(t) {}
void Bar() {/*...*/}
T mem;
//other member
};
//特例化成员Bar
template<> //表示正在特例化一个模板
void Foo<int>::Bar() //表示正在特例化Foo<int>的成员Bar
{
//其他应用于int的特例化处理
}

Welcome to my other publishing channels