文章目录
第 16 章 模板与泛型编程
16.1 定义模板
一个模板就是一个创建类或函数的蓝图或者公式。
16.1.1 函数模板
定义一个函数模板 compare:
template <typename T>
int compare(const T &v1, const T &v2) {
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
(1)实例化函数模板
编译器通常用函数实参来为我们推断模板实参。
(2)模板类型参数
类型参数前必须使用关键字 class 或 typename:
// 错误,U 之前必须加上 class 或 typename
template <typename T, U> T calc(const T&, const U&);
使用 typename 关键字更加直观。
(3)非类型模板参数
除了定义类型参数,我们还可以在模板中定义非类型参数,但其值必须是常量表达式:
template <unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1, p2);
}
(4)inline 和 constexpr 的函数模板
inline 或 constexpr 说明符放在模板参数列表之后,返回类型之前:
template <typename T>
inline T min(const T&, const T&);
(5)模板编译
当编译器遇到一个模板定义时,它并不生成代码,只有当我们使用模板时,编译器才生成代码。
函数模板和类模板成员函数的定义通常放在头文件中。
(6)大多数编译错误在实例化期间报告
模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报告错误:
-
第一个阶段:编译模板时。
在这个阶段,编译器通常不会发现很多错误。编译器可以检查语法错误,例如忘记分号或者变量名拼错等,但也就这么多了。
-
第二个阶段:编译器遇到模板使用时。
在此阶段,编译器仍然没有很多可检查的。对于函数模板调用,编译器通常会检查实参数目是否正确。它还能检查参数类型是否匹配。对于类模板,编译器可以检查用户是否提供了正确数目的模板实参,但也仅限于此了。
-
第三个阶段:模板实例化时。
只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
16.1.2 类模板
(1)定义类模板
template <typename T> class Blob {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Blob();
Blob(std::initializer_list<T> il);
// Blob 中的元素数目
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push back(const T &t) { data->push_back(t); }
// 移动版本
void push back(T &&t){ data->push_back(std::move(t)); }
void pop back();
// 元素访问
T& back();
T& operator[](size_type i);
private:
std::shared_ptr<std::vector<T>> data;
// 若 data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
};
(2)实例化类模板
当使用一个类模板时,我们必须提供额外信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
Blob<int> ia;
Blob<int> ia2 = { 0, 1, 2, 3, 4 };
ia 和 ia2 使用相同的特定类型版本的 Blob(即 Blob<int>)。从这两个定义,编译器会实例化出一个与下面定义等价的类:
template <> class Blob<int> {
typedef typename std::vector<int>::size_type size_type;
Blob();
Blob(std::initializerlist<int> il);
//...
int& operator[] (size_type i);
private:
std::shared_ptr<std::vector<int>> data;
void check(size_type i, const std::string &msg)const;
};
当编译器从我们的 Blob 模板实例化出一个类时,它会重写 Blob 模板,将模板参数 T 的每个实例替换为给定的模板实参,在本例中是 int。
(3)类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
定义在类模板之外的成员函数就必须以关键字 template 开始,后接类模板参数列表。
(4)在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参。
但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
// 若试图访问一个不存在的元素,BlobPtr 抛出一个异常
template <typename T> class BlobPtr {
public:
BlobPtr() : curr(0) { }
BlobPtr(Blob<T> &a, size_t sz = 0) : wptr(a.data), curr(sz) { }
T& operator* () const {
auto p = check(curr, "dereference past end");
return(*p)[curr]; // (*p)为本对象指向的 vector
}
// 递增和递减
BlobPtr& operator++(); //前置运算符
BlobPtr& operator--();
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr<std::vector<T>> check(std::size t, const std::string&) const;
// 保存一个 weak_ptr,表示底层 vector 可能被销毁
std::weak_ptr<std::vector<T>> wptr;
std::size_t curr; // 数组中的当前位置
};
BlobPtr 的前置递增和递减成员返回 BlobPtr&,而不是BlobPtr<T>&。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
(5)在类模板外使用类模板名
当我们在类模板外定义其成员时,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域:
// 后置:递增 / 递减对象但返回原值
template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) {
// 此处无须检查;调用前置递增时会进行检查
BlobPtr ret = *this; // 保存当前值
++*this; // 推进一个元素;前置++检查递增是否合法
return ret; // 返回保存的状态
}
在函数体内,我们已经进入类的作用域,因此在定义 ret 时无须重复模板实参。因此,ret 的定义与如下代码等价;
BlobPtr<T> ret = *this;
(6)类模板和友元
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
-
一对一友好关系
// 前置声明,在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 的模板形参作为它们自己的模板实参。因此,友好关系被限定在用相同类型实例化的 Blob 与 BlobPtr 相等运算符之间。
-
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自已的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到 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 的前置声明 };
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
-
令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元:
template <typename Type> class Bar { friend Type; // 将访问权限授予用来实例化 Bar 的类型 //... };
此处我们将用来实例化 Bar 的类型声明为友元。因此,对于某个类型名 Foo,Foo 将成为 Bar<Foo> 的友元,Sales_data 将成为 Bar<Sales_data> 的友元,依此类推。
(7)模板类型别名
typedef Blob<string> StrBlob;
// C++11 新标准方式
template <typename T> using twin = pair<T, T>;
twin<double> area; // area 是一个 pair<double, double>
(8)类模板的 static 成员
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
// 其他接口成员
private:
static std::size_t ctr;
// 其他实现成员
}
template <typename T>
size_t Foo<T>::ctr = 0; // 初始化 ctr
类似任何其他成员函数,一个 static 成员函数只有在使用时才会实例化。
16.1.3 模板参数
(1)模板声明
与函数参数相同,声明中的模板参数的名字不必与定义中相同:
// 3 个 calc 都指向相同的函数模板
template <typename T> T calc(const T&,const T&); // 声明
template <typename U> U calc(const U&,const U6); // 声明
// 模板的定义
template <typename Type>
Type calc(const Type& a, const Type& b) { /* ...*/ }
(2)使用类的类型成员
假定 T 是一个类型参数的名字,当编译器遇到如下形式的语句时:
T::size_type *pi
它需要知道我们是正在定义一个名为 p 的变量还是将一个名为 size_type 的 static 数据成员与名为 p 的变量相乘。
默认情况下,C++ 语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字 typename 来实现这一点:
template <typename T>
typename T::value_type top(const T& c) {
if(!c.empty())
return c.back();
else
return typename T::value_type();
}
(3)默认模板实参
我们也可以提供默认模板实参。在新标准中,我们可以为函数和类模板提供默认实参。而更早的 C++标准只允许为类模板提供默认实参。
例如,我们重写 compare,默认使用标准库的 less 函数对象模板:
// compare 有一个默认模板实参 less<T> 和一个默认函数实参 F()
template <typename T, typename F = less<T>>
int compare(const T &vl, const T &v2, F f = F()) {
if (f(vl, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
(4)模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。尖括号指出类必以须从一个模板实例化而来。
特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <class T = int> class Numbers { // T默认为int
public:
Numbers(T v = 0): val(V) { }
// 对数值的各种操作
private:
T val;
};
Numbers<long double> lots_of_precision;
Numbers<> average_precision; // 空<>表示我们希望使用默认类型
16.1.4 成员模板
成员模板不能是虚函数。
(1)普通(非模板)类的成员模板
与任何函数模板相同,T 的类型由编译器推断。
(2)类模板的成员模板
与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:
template <typename T> // 类的类型参数
template <typename It> // 构造函数的类型参数
Blob<T>::Blob(It b, It e) : data(std::make_shared<std::vector<T>>(b, e)) { }
(3)实例化与成员模板
与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
16.1.5 控制实例化
当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化如下:
extern template class Blob<string>; // 实例化声明
template class Blob<string>; // 实例化定义
当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明(定义)。对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此 extern 声明必须出现在任何使用此实例化版本的代码之前。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
16.2 模板实参推断
16.2.1 类型转换与模板类型参数
可以应用于模板函数的类型转换有以下两种:
-
const 转换
可以将一个非 const 对象的引用(指针)传递给一个 const 的引用(指针)形参
-
数组或函数指针转换
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换:
- 一个数组实参可以转换为指向其首元素的指针
- 一个函数实参可以转换为一个该函数类型的指针
其他类型转换(如,算术转换、派生类向基类的转换等)都不能应用于函数模板。
(1)使用相同模板参数类型的函数形参
compare 函数接受两个 const T& 参数,其实参必须是相同的类型:
long lng;
compare(lng, 1024); // 错误,不能实例化 compare(long, int)
如果希望对函数实参进行正常的类型转换,可以将函数模板定义为两个类型参数:
template <typename A, typename B>
int flexibleCompare(const A& v1, const B& v2) {
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
(2)正常类型转换应用于普通函数实参
函数模板可以有普通类型定义的参数,这种不涉及模板类型的参数,可以正常转换为对应形参的类型:
template <typename T>
ostream &print(ostream &os, const T &obj) {
return os << obj;
}
print(cout, 42); // 实例化 print(ostream&, int)
ofstream f("output");
print(f, 10); // 实例化 print(ostream&, int),将 f 转换为 ostream&
16.2.2 函数模板显示实参
(1)指定显式模板实参
我们定义表示返回类型的第三个模板参数,从而允许用户控制返回类型:
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
由于没有任何是参类型来推断 T1,因此每次调用 sum 时都必须为 T1 提供一个显式模板实参:
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
(2)正常类型转换应用于显示指定的实参
对于模板类型参数已经显示指定了的函数实参,也进行正常的类型转换:
long lng;
compare(lng, 1024); // 错误
compare<long>(lng, 1024); // 正确,compare(long, long)
compare<int>(lng, 1024); // 正确,compare(int, int)
16.2.3 尾置返回类型与类型转换
对于不清楚模板函数返回类型的情况,我们可以使用尾置返回类型,允许我们在参数列表之后声明返回类型:
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
// 处理
return *beg; // 返回引用
}
(1)进行类型转换的标准库模板类
如果我们希望上述函数返回的不是引用而是数值,那么可以使用标准库的类型转换模板,这些模板定义在头文件 < type_traits > 中:
M
o
d
T
M
o
d
<
T
>
:
:
t
y
p
e
remove_reference
X& 或 X&&
X
否则
T
add_const
X&、const X 或函数
T
否则
const T
add_lvalue_reference
X&
T
X&&
X&
否则
T&
add_rvalue_reference
X& 或 X&&
T
否则
T&&
remove_pointer
X*
X
否则
T
add_pointer
X& 或 X&&
X*
否则
T
make_signed
unsigned X
X
否则
T
make_unsigned
带符号类型
unsigned X
否则
T
remove_extent
X[n]
X
否则
T
remove_all_extents
X[n1][n2]
…
X
否则
T
\begin{array}{c} \hline \bold{Mod} & \bold{T} & \bold{Mod<T>::type} \\ \hline \text{remove\_reference}& \text{X\& 或 X\&\&} & \text{X}\\ & \text{否则} & \text{T}\\ \hline \text{add\_const} & \text{X\&、const X 或函数} & \text{T}\\ & \text{否则} & \text{const T}\\ \hline \text{add\_lvalue\_reference} & \text{X\&} & \text{T}\\ & \text{X\&\&} & \text{X\&}\\ & \text{否则} & \text{T\&}\\ \hline \text{add\_rvalue\_reference} & \text{X\& 或 X\&\&} & \text{T}\\ & \text{否则} & \text{T\&\&}\\ \hline \text{remove\_pointer} & \text{X*} & \text{X}\\ & \text{否则} & \text{T}\\ \hline \text{add\_pointer} & \text{X\& 或 X\&\&} & \text{X*}\\ & \text{否则} & \text{T}\\ \hline \text{make\_signed} & \text{unsigned X} & \text{X}\\ & \text{否则} & \text{T}\\ \hline \text{make\_unsigned} & \text{带符号类型} & \text{unsigned X}\\ & \text{否则} & \text{T}\\ \hline \text{remove\_extent} & \text{X[n]} & \text{X}\\ & \text{否则} & \text{T}\\ \hline \text{remove\_all\_extents} & \text{X[n1][n2]}\dots & \text{X}\\ & \text{否则} & \text{T}\\ \hline \end{array}
Modremove_referenceadd_constadd_lvalue_referenceadd_rvalue_referenceremove_pointeradd_pointermake_signedmake_unsignedremove_extentremove_all_extentsTX& 或 X&&否则X&、const X 或函数否则X&X&&否则X& 或 X&&否则X*否则X& 或 X&&否则unsigned X否则带符号类型否则X[n]否则X[n1][n2]…否则Mod<T>::typeXTTconst TTX&T&TT&&XTX*TXTunsigned XTXTXT
代码实现:
template <typename It>
auto fcn(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type {
// 处理
return *beg; // 返回引用
}
16.2.4 模板实参推断和引用
(1)从左值引用函数参数推断类型
-
T&
template <typename T> void f1(T&); // 实参必须是一个左值 f1(i); // i 是一个 int,模板参数类型 T 是 int f1(ci); // ci 是一个 const int,模板参数类型 T 是 const int f1(5); // 错误
-
const T&
template <typename T> void f2(const T&); // 可以接受一个右值 f2(i); // i 是一个 int,模板参数类型 T 是 int f2(ci); // ci 是一个 const int,模板参数类型 T 是 int f2(5); // 一个 const & 参数可以绑定到一个右值,T 是 int
(2)从右值引用函数参数推断类型
template <typename T> void f3(T&&);
f3(42); // 实参是一个 int 类型的右值,模板参数 T 是 int
(3)引用折叠和右值引用参数
我们通常不能将一个右值引用绑定到一个左值上,例如 f3(i)
这样的调用,我们认为是不合法的。但是 C++ 允许两种例外:
- 当将一个左值(i)传递给函数的模板类型右值引用参数(T&&)时,编译器推断模板类型参数(T)为实参的左值引用类型
- 如果间接创建了一个引用的引用(如第一个规则),则这些应用会进行折叠:
- X& &、X& &&、X&& & 都折叠成 X&
- X&& && 折叠成 X&&
正是因为这两个例外,导致了以下两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被
绑定到一个左值;且 - 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将
被实例化为一个(普通)左值引用参数(T&)
template <typename T> void f3(T&&);
f(i); // T 推断为 int&
f(ci); // T 推断为 const int&
(4)编写接受右值引用参数的模板函数
以上接受右值引用的模板函数代码对于实参类型的不同,可能会有出乎意料的结果:
template <typename T>
void f3(T&& val) {
T t = val; // T 是引用类型还是一个值类型?t 是一个引用还是一个拷贝?
t = fcn(t); // 是同时改变 val 还是只改变 t?
if (val == t) { /* ... */ } // 如果 t 是引用,则判断结果一直为 true
}
因此实际中,右值引用的函数模板通常使用以下方式进行重载:
template <typename T> void f(T&&); // 绑定到非 const 右值
template <typename T> void f(const T&); // 左值和 const 右值
16.2.5 理解 std::move
标准库的 move 定义:
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&> (t);
}
move 利用了上一节介绍的 C++ 的两个特例,其中
- 参数
T&& t
使得 move 可以接受所有类型的实参 remove_reference<T>
使得无论 T 是引用还是值类型,都返回对应的值类型- 返回类型
remove_reference<T>::type&&
使得move 总是返回一个右值引用
16.2.6 转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。
例如,我们编写一个函数,接受一个可调用表达式和两个额外的实参,并将这两个实参逆序传递给这个表达式:
// 接受一个可调用对象和另外两个参数的模板
// 对"翻转"的参数调用给定的可调用对象
// flip1 是一个不完整的实现:顶层 const 和引用丢失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 tl, T2 t2) {
f(t2, t1);
}
当我们传递的实参是引用时,可以发现,f(t2, t1)
传递的可能是值,因此并没有达到原始期待的效果。
(1)使用右值形参优化
按照之前介绍的内容,我们可以使 T 变为 T&&:
template <typename F, typename T1, typename T2>
void flip2(F f, T1&& tl, T2&& t2) {
f(t2, t1);
}
这样优化的代码可以很好地保持翻转实参的 const、左值和右值属性。
但是这样的版本只能解决一半的问题,却不能处理接受右值引用参数的函数。
假设我们传进去的表达式 g 定义如下:
void g(int &&i, int &j) {
cout << i << " " << j << endl;
}
flip2(g, i, 42); // 错误,不能用左值实例化 int&&
因为传进去的实参为 (g, i, 42)
,因此 T2 绑定的类型为 int,但是在 flip2 函数中,将 t2(左值)传递给了 g 的形参 i(右值),这明显是不允许的。
(2)使用 std::forward 保持类型信息
我们可以使用一个名为 forward 的新标准库设施来传递 flip2 的参数,它能保持原始实参的类型。其定义在头文件 utility 中。forward 必须通过显式模板实参来调用。forward 返回该显式实参类型的右值引用。即,forward<T>的返回类型是 T&&。
通常情况下,我们使用 forward 传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward 可以保持给定实参的左值 / 右值属性:
template <typename Type> intermediary (Type &&arg) {
finalFcn (std::forward<Type>(arg));
//...
}
对于我们的例子而言,修改代码如下:
template <typename F, typename T1, typename T2>
void flip1(F f, T1&& tl, T2&& t2) {
f(std::forward<T2> (t2), std::forward<T2> (t1);
}
16.3 重载与模板
涉及函数模板的函数匹配规则如下:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比
其他模板更特例化,则选择此模板。 - 否则,此调用有歧义。
(1)编写重载模板
作为一组例子,我们编写一组调试函数 debug_rep,每个函数都返回一个给定对象的 string 表示:
// 打印任何我们不能处理的类型
template <typename T> string debug_rep(const T &t) {
ostringstream ret;
ret << t; // 使用 T 的输出运算符打印 t 的一个表示形式
return ret.str(); // 返回 ret 绑定的 string 的一个副本
}
// 打印指针的值,后跟指针指向的对象
// 注意:此函数不能用于 char*
template <typename T> string debug_rep(T *p) {
ostringstream ret;
ret << "pointer: " << p; // 打印指针本身的值
if (p)
ret << " " << debug_rep(*p); // 打印 p 指向的值
else
ret<< " null pointer"; // 或指出 p 为空
return ret.str(); // 返回 ret 绑定的 string 的一个副本
}
一些函数的调用:
string s("hi");
cout << debug_rep(s) << endl; // 调用 debug_rep(const T &t)
cout << debug_rep(&s) << endl; // 调用 debug_rep(T *p)
(2)多个可行模板
考虑一个特殊的例子:
const string *sp = &s;
cout << debug_rep(sp) << endl; // 调用 debug_rep(T *p)
此例中的两个模板都是可行的,而且两个都是精确匹配:
-
debug_rep(const string *&)
由第一个版本的 debug_rep 实例化而来,T 被绑定到 string*。
-
debug_rep(const string*)
由第二个版本的 debug_rep 实例化而来,T 被绑定到 const string。
在此情况下,正常函数匹配规则无法区分这两个函数。我们可能觉得这个调用将是有歧义的。但是,根据重载函数模板的特殊规则,此调用被解析为 debug_rep(T*),即,更特例化的版本。
设计这条规则的原因是,没有它,将无法对一个 const 的指针调用指针版本的 debug_rep。问题在于模板 debug_rep(const T&)本质上可以用于任何类型,包括指针类型。此模板比 debug_rep(T*) 更通用,后者只能用于指针类型。没有这条规则,传递 const 的指针的调用永远是有歧义的。
(3)非模板和模板重载
作为下一个例子,我们定义一个普通非模板版本的 debug_rep 来打印双引号保卫的 string:
// 打印双引号包围的 string
string debug_rep(const string &s) {
return '"' + s + '"';
}
现在对一个 string 调用 debug_rep:
string s("hi");
cout << debug_rep(s) << endl; // 调用 debug_rep(const string &s)
有两个同样好的可行函数:
-
debug_rep<string>(const string&)
第一个模板,T 被绑定到 string*。
-
debug_rep(const string&)
普通非模板函数。
但是,编译器会选择非模板版本,因为其更特例化,一个非模板函数比一个函数模板更好。
(4)重载模板和类型转换
考虑 C 风格字符串的匹配:
cout << debug_rep("hi world!") << endl; // 调用 debug_rep(T*)
其实对于三个版本的 debug_rep 都是可行的:
-
debug_rep(const T&)
T被绑定到 char[10]。
-
debug_rep (T*)
T 被绑定到 const char。
-
debug_rep(const string&)
要求从 const char* 到 string 的类型转换。
同样,T* 版本更加特例化,因此编译器会选择它。
16.4 可变参数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。
- **参数包:**可变数目的参数
- **模板参数宝包:**表示 0 个或多个模板参数
- **函数参数包:**表示 0 个或多个函数参数
// Args 是一个模板参数包;rest 是一个函数参数包
// Args 表示零个或多个模板类型参数
// rest 表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args& ...rest);
编译器从函数的实参推断模板参数的类型,并且还会推断包中参数的数目:
int i = 0; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // 包中有三个参数
foo(s, 42, "hi"); // 包中有两个参数
foo(d, s); // 包中有一个参数
foo("hi"); // 空包
编译器会实例化以下几个版本:
void foo(const int&, const string&, const int&, const double&);
void foo(const string&, const int&, const char[3]&);
void foo(const double&, const string&);
void foo(const char[3]&);
当我们需要直到包中有多少个元素时,可以使用 sizeof… 运算符,返回一个常量表达式:
template <typename ... Args> void f(Args... args) {
cout << sizeof...(Args) << endl;
cout << sizeof...(args) << endl;
}
16.4.1 编写可变参数函数模板
// 用来终止递归并打印最后一个元素的函数
// 此函数必须在可变参数版本的 print 定义之前声明 、
template<typename T>
ostream &print(ostream &os, const T &t) {
return os << t; // 包中最后一个元素之后不打印分隔符
}
// 包中除了最后一个元素之外的其他元素都会调用这个版本的 print
template <typename T, typename...Args>
ostream &print(ostream &os, const T &t, const Args&...rest) {
// 打印第一个实参
os << t<< ",";
// 递归调用,打印其他实参
return print(os, rest...);
}
16.4.2 包扩展
对于一个参数包,除了获取其大小外,我们能对它做的唯一的事情就是扩展它。
当扩展一个包时,我们还要提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式右边放一个省略号(…)来触发扩展操作。
// 在 print 调用中对每个实参调用 debug_rep
template <typename... Args>
ostream &errorMsg(ostream &os, const Args&...rest) {
// print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(an))
return print(os, debug_rep(rest)...);
}
这个 print 调用使用了模式 debug_reg(rest)。此模式表示我们希望对函数参数包 rest 中的每个元素调用 debug_rep。扩展结果将是一个逗号分隔的 debug_rep 调用列表:
errorMsg(cerr, fcnName, code.num(), otherData, "other", item);
就好像我们这样编写代码一样
print(cerr, debug_rep(fcnName), debug_rep(code.num(),
debug_rep(otherData), debug_rep("otherData"),
debug_rep(item));
与之相对,下面的模式会编译失败:
// 将包传递给 debug_rep; print(os, debug_rep(a1,a2,...,an))
print(os, debug_rep(rest...)); // 错误:此调用无匹配函数
这段代码的问题是我们在 debug_rep 调用中扩展了 rest,它等价于
print(cerr, debug_rep(fcnName, code.num(), otherData, "otherData", item));
16.5 模板特例化
在某些情况下,通用模板的定义对特定类型是不适合的。其他时候,我们可以定义类或函数模板的一个特例化版本。以 compare 函数为例:
// 第一个版本;可以比较任意两个类型
template <typename T> int compare(const T&, const T&);
// 第二个版本处理字符串字面常量
template <size_t N,size t M>
int compare(const char (&)[N], const char (&)[M]);
但是,如果传入两个指向字符串常量的指针,编译器仍将调用第一个版本:
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // 调用第一个模板
compare("hi", "mom"); // 调用第二个模板
因此,我们还需要为这种情况进行特例化处理.
(1)定义函数模板特例化
特例化模板时,需要提供所有实参,并且在 template 后面紧跟 <> 表示我们将提供所有的实参:
// compare 的特殊版本,处理字符数组的指针
template <>
int compare(const char* const &p1, const char* const *p2) {
return strcmp(p1, p2);
}
这里为了对应之前模板 template <typename T> int compare(const T&, const T&)
的特例化,我们需要一个指向 const char* 的引用。实际上,T 对应的就是 const char*,因此我们定义的形参类型应该是 const char* const &,这里第二个 const 对应 const T&
中的 const。
(2)函数重载与模板特例化
当定义函数模板的特例化版本时,我们本质上接管了编译器的工作。即,我们为原模板的个特殊实例提供了定义。重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
特例化的本质是实例化一个模板,而非重载它。因此,特例化不影响函数匹配。