第 16 章 模板与泛型编程
标签: C++Primer 学习记录 模板 泛型编程
在做这一章的笔记时,因为有很多内容也是在看 C++ Primer这本书时第一次接触到,所以需要记录大段文字。挨个字敲,又太累,所以就想有没有什么高效的输入手段。后面想到了语音输入,对比了搜狗输入法和讯飞输入法,发现讯飞输入法对于专业术语也能翻译的很好。这样一来,遇到整段文字就再也不用烦心了。果然,想偷懒,才能提高效率嘛!
16.1 定义模板
-
面向对象编程能处理类型在程序运行之前都未知的情况,动态联编。而泛型编程中,在编译时就能获知其类型,静态联编。
-
类型参数前必须使用关键字 class或 typename,并且使用 typename指定模板类型参数更为直观。
// 错误,U之前必须加上 class或 typename template <typename T, U> T calc(const T&, const U&);
-
除了定义类型参数,还可以在模板中定义非类型参数,通过一个特定的类型名而非关键字 class或 typename来指定非类型参数。因为编译器需要在编译时实例化模板,此时非类型参数会被一个用户提供的或编译器推断出的值所代替,所以这些值必须是常量表达式。
- 非类型参数可以是一个整型,对应的模板实参必须是常量表达式。而在模板定义内,可以将这个非类型参数用在任何需要常量表达式的地方,如指定数组大小。
template <unsigned N, unsigned M> int compare(const cahr (&p1)[N], const cahr (&p2)[M]) [ return strcmp(p1, p2); } compare("hi", "mom"); // 上式调用会实例化处如下版本,注意字符串字面常量的末尾有一个空字符! int compare(const cahr (&p1)[3], const cahr (&p2)[4])
- 也可以是一个指向对象或函数类型的指针或(左值)引用。绑定到指针或引用非类型参数的实参必须具有静态的生存期。
-
函数模板可以声明为 inline或 constexpr的,inline或 constexpr说明符需要放在模板参数列表之后,返回类型之前。
// 正确 template <typename T> inline T min(const T&, const T&); // 错误 inline template <typename T> T min(const T&, const T&);
-
为了提高适用性,模板程序应尽量减少对实参类型的要求。
- 模板中的函数参数是 const的引用。这样做一方面保证了即使参数类型不支持拷贝,模板程序也能正确运行;另一方面引用不会引起对象的拷贝构造,提高运行性能。
- 模板中使用到的类型相关的函数或运算符应尽可能的少。
-
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板不能分离式编译,其头文件中通常既包括声明也包括定义。
-
模板直到实例化时才会生成代码,大多数编译错误在实例化期间报告。通常,编译器会在三个阶段报告错误。
- 第一个阶段是编译模板本身时。这个阶段,编译器可以检查语法错误,如忘记分号或者变量名拼错等。
- 第二个阶段是编译器遇到模板使用时。对于函数模板调用,会检查实参数目是否正确和参数类型是否匹配。对于类模板,则只检查模板实参数目是否正确。
- 第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
-
编译器不会为类模板推断模板参数类型,使用时,必须显式提供模板实参。
-
一个类模板的每个实例都形成一个独立的类,类型 Blob与任何其他 Blob类型都没有关联,也不会对任何其他 Blob类型的成员有特殊访问权限。
// 下面的定义实例化出两个不同的 Blob类型 Blob<string> names, titles; // 同一个类型的不同对象 Blob<double> prices;
-
定义在类模板之外的成员函数的书写形式:
template <typename T>
ret-type Blob<T>::member-name(parm-list)
-
默认情况下,一个类模板的成员函数只有当程序用到它时才进行实例化。这一特性使得即使某种类型不能完全符合模板操作的要求,仍然能用该类型实例化类,但相应操作无法使用!
-
在一个类模板的作用域内,可以直接使用模板名而不必指定模板实参。其他情况下都必须提供模板实参。
template <typename T>
// 返回类型,处于类的作用域之外,需要提供模板实参
BlobPtr<T> BlobPtr<T>::operator++(int)
{
// 函数体内,处于类的作用域之内
BlobPtr ret = *this;
...
}
- 如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例;如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
- 一对一友好关系。用相同模板实参实例化的友元是该类的友元,可以访问非 public部分,而对于用其他实参实例化的实例则没有特殊访问权限。
// 为了在 Blob中声明友元,需要前置声明
template<typename T> class BlobPtr;
template<typename T> class Blob; // 声明运算符 ==中的参数所需要的
template<typename T>
bool operator==(const Blob<T> &lhs, const Blob<T> &rhs);
template <typename T>
class Blob
{
// 每个 Blob实例将访问权限授予用相同类型实例化的 BlobPtr和相等运算符
friend class BlobPtr<T>;
friend bool operator==<T>
(const MyBlobPtr &lhs, const MyBlobPtr &rhs);
// 其它成员定义
};
// BlobPtr<char>的成员可以访问 ca(或任何其它 Blob<char>对象)的非 public部分
Blob<char> ca;
Blob<int> ia;
- 通用和特定的模板友好关系。为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
// 前置声明,在将模板的一个特定实例声明为友元时要用到
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 T> using twin = pair<T, T>;
twin<int> win_loss; // win_loss是一个 pair<int, int>
template <typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books pair<string, unsigned>
- 对于类模板 Foo中的 static成员 ctr,对于任意给定类型 X,都有一个Foo::ctr成员。所有 Foo类型的对象共享相同的 ctr成员。
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
// 其它接口成员
private:
static std::size_t ctr;
// 其它数据成员
};
// 所有三个对象共享相同的 Foo<int>::ctr和 Foo<int>::count成员
Foo<int> fi, fi2, fi3;
- 类模板的 static成员,可以通过类类型对象来访问,也可以用作用域运算符直接访问该成员,不过必须提供一个特定的模板实参。另外,static成员函数也是只在使用时才会被初始化。
Foo<int> fi; // 实例化 Foo<int>类和 static数据成员 ctr
auto ct = Foo<int>::count(); // 实例化 Foo<int>::count
ct = fi.count(); // 使用 Foo<int>::count
ct = Foo::count(); // 错误,无法确定使用哪个模板实例化的 count
- 模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。模板参数会隐藏外层作用域中声明的相同名字,但是在模板内不能重用模板参数名。
typedef double A;
template <typename A, typename B> void f(A a, B b)
{
A tmp = a; // tmp的类型为模板参数 A的类型,而非 double
double B; // 错误,重声明模板参数 B
}
- 模板声明必须包含模板参数,声明中的模板参数的名字不必与定义中相同。
template <typename T> class Blob; // 声明但不定义
- 默认情况下,C++语言假定通过作用域运算符访问的名字不是类型,而是一个 static数据成员。如果想使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型,可以使用关键字 typename来实现这一点。
template <typename T>
// 返回一个成员类型
typename T::value_type top(const T &c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}
- 模板也可以提供默认模板实参,对于一个模板参数,只有当它右侧的所有参数都有默认实参时,它才可以有默认实参。
// compare有一个默认模板实参 less<T>和一个默认函数实参 F()
int compare(const T &v1, const T &v2, F f = F())
{
if (f(v1, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
- 如果一个类模板为其所有模板参数都提供了模板实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template <typename T = int> class Numbers {
public:
Numbers(T v = 0) : val(v) { }
// 对数值的各种操作
private:
T val;
}
Numbers<long double> lots_of_precision;
Numbers<> integer;
- 一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数,这种成员函数被称为成员模板,成员模板不能是虚函数。
- 普通(非模板)类的成员模板。
// 函数对象类,对给定指针执行 delete
class DebugDelete
{
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) {}
template<typename T> void operator()(T *p) const
{
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::ostream &os;
};
// 在一个临时 DebugDelete对象上调用 operator()(double*)
double *p = new double;
DebugDelete() (d);
// 重载 unique_ptr的删除器,在尖括号内给出删除器类型,并在构造函数中提供一个这种类型的对象
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// 在销毁 p指向的对象时,实例化 DebugDelete::operator<int>(int *)
- 类模板也可以定义其成员模板,此时,类和成员各自有自己的、独立的模板参数。成员模板是函数模板,在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
template <typename T> class Blob {
template <typename It> Blob(It b, It e);
//...
};
// 类外定义
template <typename T> // 类的类型参数
template <typename It> // 构造函数的类型参数
Blob<T>::Blob(It b, It e) : data(make_shared<vector<T>>(b, e)) {}
- 模板被使用时才会进行实例化,这意味着,当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。这可能会带来很严重的额外开销,可以通过显式实例化来避免这种开销。在声明和定义中,所有模板参数已被替换为模板实参。
extern template class Blob<string> // 声明
template int compare(const int&, const int&); // 定义
- 当编译器遇到 extern模板声明时,它不会在本文件中生成实例化代码。对于一个给定的实例化版本,可能会有多个 extern声明,但必须只有一个定义。由于编译器在使用一个模板时自动对其实例化,因此 extern声明必须出现在任何使用此实例化版本的代码之前:
// Application.cc
// 这些模板类型必须在程序其它位置进行实例化
extern template class Blob<string>;
extern template int compare(const int&, const int&);
Blob<string> sa1, sa2; // 实例化会出现在其他位置
// Blob<int>及其接受 initializer_list的构造函数在本文件中实例化
Blob<int> a1 = {0, 1, 2, 3, 4};
Blob<int> a2(a1); // 拷贝构造函数在本文件中实例化
int i = compare(a1[0], a2[0]); // 实例化出现在其他位置
// templateBuild.cc
// 实例化文件必须为每个在其他文件中声明为 extern的类型和函数提供一个(非 extern)的定义
template int compare(const int&, const int&);
template class Blob<string>;
-
与类模板的普通实例化不同,类模板的显式实例化定义会实例化该模板的所有成员。因此,用来显示实例化一个类模板的类型,必须能用于模板的所有成员。
-
shared_ptr,因为不同对象可以共享指针所有权,需要在运行时可以方便的重载删除器;unique_ptr,独占指针,不需要重载删除器,自定义删除器的类型需要在定义 unique_ptr时一并给出。
16.2 模板实参推断
-
只有很有限的几种类型转换会自动地应用于模板实参,编译器通常不是对实参进行类型转换,而是生成一个新的模板实例。
- 顶层 const,无论是在形参还是实参中,都会被忽略。
- const转换,可以将一个非 const对象的引用(或指针)传递给一个 const的引用(或指针)形参。
- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。
- 其它类型转换,如算数转换、派生类向基类的转换以及用户定义的转换都不能应用于函数模板。
template <typename T> T fobj(T, T) // 实参被拷贝 template <typename T> T fref(const T&, const T&) // 引用 string s1("a value"); const string s2("another value"); fobj(s1, s2); // 调用 fobj(string, string),const被忽略 fref(s1, s2); // 调用 fobj(const string&, cosnt string&) int a[10], b[42]; fobj(a, b); // 调用 f(int*, int*) fref(a, b); // 错误,数组大小不同,是不同类型,与模板参数类型不匹配
-
函数模板可以有用普通类型定义的参数,即不涉及模板类型参数的类型。对于这种参数,对实参进行正常的类型转换。
-
当函数返回类型与参数列表中任何类型都不相同时,编译器无法推断出模板实参的类型或者希望允许用户控制模板实例化,可以指定显式模板实参。显式模板实参按由左至右的顺序与对应的模板参数匹配,推断不出的模板参数的类型在定义时应该放在参数列表的最左边。
template <typename T1, typename T2, typename T3> T1 sum(T2, T3); // T1是显式指定的, T2和 T3是从函数实参类型推断而来的 auto val3 = sum<long long>(i, lng); // 实例化为 long long sum(int, long); // 糟糕的设计,用户必须指定所有三个模板参数 template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1); // 错误,不能推断出 T3 auto val3 = alternative_sum<long long>(i, lng);
-
对于模板类型参数已经显式指定了的函数实参,可以进行正常的类型转换。
long lng; compare(lng, 1024); // 错误,模板参数不匹配 compare<long>(lng, 1024); // 正确,1024自动转化为 long
-
对于函数返回类型不容易确定的情况,使用尾置返回类型,更加方便高效。并且如果需要类型转换,可以使用标准库的类型转换模板。
// 返回一个序列中的元素值 // 为了使用模板参数的类型成员,必须使用 typename template <typename It> auto fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type; { // 处理序列 return *beg; }
-
使用函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。如果不能从函数指针类型确定模板实参,则产生错误。
template <typename T> int compare(const T&, const T&); // func的重载版本,每个版本接受一个不同的函数指针类型 void func(int(*)(const string&, const string&)); void func(int(*)(const int&, const int&)); func(compare); // 错误,不能确定使用哪一个实例 // 正确的做法是可以显式指出实例化哪个版本 func(compare<int>);
-
从左值引用函数参数推断类型。
template <typename T> void f(T &p)
。实参必须是一个左值。如果实参是 const的,则 T将被推断为 const类型。
// 对 f1的调用使用实参所引用的类型作为模板参数类型 f1(i); // i是 int,模板参数 T是 int,函数参数是 int& f1(ci); // ci是 const int,模板参数 T是 const int,函数参数是 const int& f1(5); // 错误
template <typename T> void f(const T &p)
。实参可以是任意类型(包括右值在内),即使实参是 const的,T的推断类型也不会是一个 const类型。
// 在下列调用中,函数参数都是 const f2(i); // i是 int,模板参数 T是 int f2(ci); // ci是 const int,模板参数 T是 int f2(5); // 实参为 int类型的右值,模板参数 T是 int
-
从右值引用函数参数推断类型。
- 传递的实参为右值。推断出的 T的类型是该右值实参的类型。
template <typename T> void f3(T&&); f3(42); // 实参为 int类型的右值,模板参数 T是 int
- 传递的实参为左值。此时得到的模板参数和函数参数都是左值引用。
f3(i); // 实参是 int类型的左值,模板参数 T是 int& f3(ci); // 实参为 const int类型的左值,模板参数 T是 const int&
-
对于接受右值引用参数的模板函数,当分别传递右值和左值实参时,模板参数类型可能是普通类型,也可能是引用类型。有时这可能会造成意想不到的结果。解决这种问题的办法是,使用基于函数参数的模板重载,来将实参分别为右值或左值时的情况分离开来。
template <typename T> void f3(T&&) { T t = val; // 实参为右值时,赋值语句 // 实参为左值时,绑定引用 t = fcn(t); // 实参为右值时,只改变 t // 实参为左值时,既改变 t,也改变 val } // 定义一组重载函数,解决上述问题 template <typename T> void f(T&&); // 绑定到非 const右值 template <typename T> void f(const T&); // 绑定到左值和 const右值
-
某些函数需要将其一个或多个实参连同类型不变地转发给其它参数,需要保持转发实参的所有性质,包括实参类型是否是 const的以及实参是左值还是右值。
// 该模板将两个额外参数逆序传递给指定的可调用对象
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t1);
}
// flip1一般情况下工作的很好,但是当用它调用一个接受引用参数的函数时会出现问题
void f(int v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
f(42, i); // f改变了实参 i
flip1(f, j, 42) // j的值不会改变
- 如果一个函数参数是指向模板类型参数的右值引用(如 T&&),它对应的实参的 const属性和左值/右值属性将得到保持。使用这种方案改写上面的 flip1函数。
// 该模板将两个额外参数逆序传递给指定的可调用对象
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
// flip2对接受左值引用函数工作的很好,但不能用于接受右值引用的函数
void g(int &&v1, int &v2)
{
cout << v1 << " " << v2 << endl;
}
g(42, i); // 正确
flip1(g, i, 42) // 错误,g中接收到的 “42”是左值
- 当用于一个指向模板参数类型的右值引用函数(T&&)时,
forward
会保持实参类型的所有细节。与 move不同,forward必须通过显式模板实参来调用。下面使用 forward重写翻转函数。
template<typename F, typename T1, typename T2>
void flip3(F f, T1 &&t1, T2 &&t2)
{
f( std::forward<T2>(t2), std::forward<T1>(t1) );
}
16.3 重载与模板
- 函数模板可以被另一个模板或一个普通非函数模板重载,与往常一样,名字相同的函数,必须具有不同数量或类型的参数。如果涉及函数模板,则函数匹配规则会在以下几个方面受到影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其它模板更特例化则选择此模板。
- 否则,此调用有歧义。
// 通用模板,返回 T型对象t的 string表示
template <typename T>
string debug_rep(const T &t)
{
std::ostringstream ret;
ret << t;
return ret.str();
}
// 通用模板,返回 T型指针 p的 string表示
template <typename T>
string debug_rep(T *p)
{
std::ostringstream ret;
// 打印指针本身的值
ret << "pointer: " << p;
// p不为空,则打印 p指向的值
if (p)
ret << " " << debug_rep(*p);
else
ret << " null pointer";
return ret.str();
}
// 对于下面的代码调用,只会使用第一个模板
string s("hi");
cout << debug_rep(s) << endl;
// 对于下面的代码调用,最终会调用第二个模板,具体原因见下面第 2条。
cout << debug_rep(&s) << endl;
// 对于下面的代码调用,最终会调用第二个模板,具体原因见下面第 3条。
const string *sp = "hi";
cout << debug_rep(sp) << endl;
// 再定义一个普通非模板函数,打印双引号包围的 string
string debug_rep(const string &s)
{
cout << '"' + s + '"';
}
// 对于下面的代码调用,会使用普通非模板函数
cout << debug_rep(s) << endl;
// 对于下面的代码调用,最终会调用第二个模板,具体原因见下面第 4条。
cout << debug_rep("hi") << endl;
-
对于第一个模板参数
const T &t
,当实例化string *
参数时,模板参数是string *
,而函数参数是string * const &t
,表示 t是引用,引用自 string型指针(本身是常量)。在进行模板实参推断之后会进行普通函数的函数匹配过程。而string * const &t
中的顶层const
属性也会被略去,即f(string * const &t)
和f(string *t)
存在二义性。此时后者更特例化,所以编译器实际执行的是后者。 -
对于第一个模板参数
const T &t
,当实例化const string *
参数时,模板参数是const string *
,而函数参数是const string * const &t
,表示 t是引用,引用自 string型指针(指向常量,且本身是常量)。所以,同样地,f(const string * const &t)
和f(const string *t)
存在二义性。此时后者更特例化,所以编译器实际执行的是后者。 -
对于第一个模板,T的类型为 char[3];对于第二个模板,T的类型是 const char;对于普通非模板函数,要求从 const char*到 string的类型转换。此时,3个候选函数都是可行的。普通函数由于需要进行类型转换,可以首先排除掉。而剩下两个模板函数,后者更特例化,所以编译器实际执行的是后者。
-
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到你希望调用的函数,而实例化一个并非你所需的版本。
template <typename T> string debug_rep(const T &t); template <typename T> string debug_rep(T *p); // 为了使 debug_rep(char*)的定义正确工作,下面的声明必须在作用域中 string debug_rep(const string &); string debug_rep(char *p) { // 如果接受一个 const string&的版本的声明不在作用域中, // 返回语句将调用 debug_rep(const T &t)的 T实例化为 string的版本 return debug_rep(string(p)); }
16.4 可变参数模板
-
一个可变参数模板,就是一个接受可变输入参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包表示零个或多个函数参数。
-
在一个模板参数列表中,class...或 typename...指出,接下来的参数表是零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。在函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
// Args是一个模板参数包; rest是一个函数参数包 // Args表示零个或多个模板类型参数 // rest表示零个或多个函数参数 template <typename T, typename... Args> void foo(const T &t, const Args& ... rest); // 对于下面调用 int i = 0; foo(i, "hi"); // 包中有一个参数,实例化为 foo(const int &, const char[3]&); foo("hi"); // 空包,实例化为 foo(const char[3]&);
-
sizeof...运算符可以返回一个常量表达式,表示包中的元素个数,而且不会对其实参求值。
template<typename... Args> void g(Args... args) { cout << sizeof...(Args) << endl; // 类型参数的数目 cout << sizeof...(args) << endl; // 类型参数的数目 }
-
initializer_list用来表示一组类型相同的可变数目参数,而当类型也是未知时,则需要使用可变参数函数模板。可变参数函数通常是递归的,第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
// 用来终止递归并打印最后一个元素的函数 // 此函数必须在可变参数版本的 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...); // 递归调用,打印其他实参 }
-
给定
print(cout, i, s, 42)
,其调用过程如下:
-
对于最后一次递归调用 print(cout, 42),两个 print版本都是可行的。但是因为非可变参数模板比可变参数模板更特例化,因此编译器选择非可变参数版本。另外,定义可变参数版本的print时,非可变参数版本的声明必须在作用域中,否则,可变参数版本会无限递归。
-
当扩展一个包时,可以提供用于每个扩展元素的模式。扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边放一个省略号(...)来触发扩展操作。
template<typename T, typename... Args> ostream& print(ostream &os, const T &t, const Args&... rest) // 扩展 Args { os << t << ", "; return print(os, rest...); // 扩展 rest } // 对 Args的扩展中,将模式 const Arg&应用到模板参数包 Args中的每个元素 print(cout, i, s, 42); // 实例化的形式为 ostream& print(ostream &, const int&, const string&, const int&);
-
print中的函数参数包扩展仅仅将包扩展为其构成元素,还可以进行更复杂的扩展模式。比如,对其每个实参调用之前出现过的 debug_rep。
template<typename... Args> ostream& errorMsg(ostream &os, const Args&... rest) { print(os, debug_rep(rest)...); // 上式等价于 print(os, debug_rep(a1), debug_rep(a2), ..., debug_rep(a3)); // 注意,不可以写成下式形式 print(os, debug_rep(rest...)); // 错误,此调用无匹配函数 return os; }
-
可变参数函数通常将它们的参数转发给其他函数,这种函数具有与容器中的 emplace_back函数一样的形式。work调用中的扩展既扩展了模板参数包也扩展了函数参数包。
// fun有零个或多个参数,每个参数都是一个模板参数类型的右值引用 template<typename... Args> void fun(Args&&... args) // 将 Args扩展为一个右值引用的列表 { // work的实参既扩展 Args又扩展 args work( std::forward<Args>(args)... ); }
16.5 模板特例化
-
在某些情况下,通用模板的定义可能编译失败、做的不正确,或者利用特定知识来编写更高效的代码,而不是从通用模板实例化。这时可以定义类或函数模板的一个特例化版本。
-
当我们特例化一个函数模板时,必须为元模板中的每个模板参数都提供实参。为了指出我们正在实例化一个模板,应使用关键字 template后跟一个空尖括号对(<>)。空尖括号指出我们将为原模板的所有模板参数提供实参。
// 第一个版本,可以比较任意两个类型 template <typename T> int compare(const T&, const T&); // 第二个版本,处理字符串字面常量 template <size_t N, size_t M> int compare(const cahr (&p1)[N], const cahr (&p2)[M]); const char *p1 = "hi", *p2 = "mom"; compare(p1, p2); // 调用第一个版本 compare("hi", "mom"); // 调用第二个版本 // compare的特例化版本,处理字符数组的指针 template <> int compare(const char* const &p1, const char* const &p2) { return strcmp(p1, p2); } // 参数类型为指针,不能调用第二个版本,这里调用的是特例化版本 compare(p1, p2);
-
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
-
类模板特例化。作为例子,这里为 Sales_data类定义特例化版本的 hash模板。而定义了 hash模板的特例化版本的类类型,可以存储在无序容器中。为了让 Sales_data类的用户能使用 hash的特例化版本,应该在 Sales_data的头文件中定义该特例化版本。一个特例化 hash类必须定义:
- 一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个 size_t。
- 两个类型成员,result_type和 argument_type,分别表示调用运算符的返回类型和参数类型。
- 默认构造函数和拷贝赋值运算符(可以隐式定义)。
template <typename T> struct std::hash; class Sales_data { friend struct std::hash<Sales_data>; // 其它数据成员 }; // 为了使 Sales_data能存储在无序容器中,特例化 hash模板 // 注意, Sales_data类应支持 == 操作 namespace std { template <> struct hash<Sales_data> { typedef size_t result_type; typedef Sales_data argument_type; size_t operator()(const Sales_data &s) const; }; inline size_t hash<Sales_data>::operator()(const Sales_data & s) const { std::cout << "hash模板的 Sales_data特例化版本" << std::endl; return hash<string>()(s.bookNo) ^ hash<unsigned>()(s.units_sold) ^ hash<double>()(s.revenue); } }
-
可以指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。只能部分特例化类模板,而不能部分特例化函数模板。
template <typename T> struct Foo { Foo(cosnt T &t = T()) : men(t) {} void Bar() { /* ... */ } T men; // Foo的其他成员 }; template<> // 表示正在特例化一个模板 void Foo<int>::Bar() // 正在特例化 Foo<int>的成员 Bar { // 进行应用于 int的特例化处理 } Foo<string> fs; // 实例化 Foo<string>::Foo() fs.Bar(); // 实例化 Foo<string>::Bar() Foo<int> fi; // 实例化 Foo<int>::Foo() fi.Bar(); // 使用特例化版本的 Foo<int>::Bar()