C++_Primer16.template
模板与泛型编程
OOP和泛型编程都能处理在编写程序时不知道类型的情况。区别是:OOP能处理类型在程序运行之前都未知的情况;而泛型编程中,在编译时就能获知类型了。
模板是C++泛型编程的基础。一个模板就是一个创建类或函数的蓝图或公式。
使用泛型时,将蓝图转换为特定的类或函数,这种转换发生在编译时。
定义模板
函数模板
template <typename T>
int compare(const T& v1, const T& v2) {
if (less<T>(v1, v2)) return -1;
if (less<T>(v2, v1)) return 1;
return 0;
}
cout << compare(1, 0) << endl; // T 为 int
<typename T> 是模板参数列表,用逗号分隔的一个或多个模板参数的列表,用<>包围起来,不能为空。
当使用模板时,我们隐式或显式地指定模板实参,将其绑定到模板参数上。
编译器根据模板实参来推断类型,将其绑定到模板参数T上,然后生成一个特定版本的函数,称为模板的实例。
compare 函数体中之所以用less模板函数,而不用小于号,是因为T可能是指针类型,指针的直接比较是地址的比较,没有意义。
类型模板参数
多个类型参数:
// 模板参数列表中,typename和class没有什么不同;typename比class更直观
template <typename T, class U> calc (const T&, const U&);
typename更清楚地指出随后的名字是一个类型名。但是typename 是在模板已经广泛使用后才引入C++语言的。某些程序员仍然只用class
非类型模板参数
非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字typename或class来指定非类型参数。
当模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。
非类型参数可以是一个整型,也可以是一个指向对象或函数类型的指针或(左值)引用。
绑定到非类型整型参数的实参必须是一个常量表达式;
绑定到指针或引用非类型参数的实参必须具有静态的生存期。
指针参数也可以用 nullptr 或 0 的常量表达式来实例化。
// N, M 分别代表两个数组的长度
// 数组是无法拷贝的,所以传递引用,相当于首元素指针的引用
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1, p2);
}
compare("hi", "mom");
编译器会使用字面常量的大小代替N和M,编译器会实例化出如下版本:
int compare(const char (&p1)[3], const char (&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&);
模板编译
通常,普通类和函数的声明放在头文件中,定义放在源文件中,这是因为当调用一个函数时,编译器只需要掌握函数的声明,而其定义不必已经出现。
而模板则不同,为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。所以函数模板和类模板成员函数的定义通常放在头文件中。
模板的设计者应该提供一个头文件,包含模板定义和类模板或成员定义中用到的所有名字的声明。
编译错误的报告
编译器会在三个阶段报告错误:
- 编译模板本身时
- 语法错误
- 模板使用时
- 函数模板的实参数目是否正确,参数类型是否匹配
- 对于类模板,检查用户是否提供了正确数目的模板实参
- 模板实例化时
- 类型相关错误,依赖于编译器如何管理实例化,这类错误可能在链接时才报告
编写模板时,代码是不能针对特定类型的,但模板代码通常对其所使用的类型有一些假设。比如如果compare中的比较使用的是小于号,而传入的模板类型参数是某个类 Sales_data,而且该类没有定义<运算符:
// compare 函数体
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
// 调用,Sales_data 未定义<
Sales_data d1, d2;
cout << compare(d1, d2) << endl;
则此实例化生成了一个无法编译通过的函数版本。这样的错误直至编译器在类型 Sales_data 上实例化compare时才会被发现。
保证传递给模板的实参支持模板所要求的操作,以及这些操作在模板中能正确工作,是调用者的责任。
类模板
与函数模板不同的是,编译器不能为类模板推断模板参数类型。
在使用类模板时,必须提供额外信息,用来代替模板参数的模板实参列表。
一个类模板的每个实例都形成一个独立的类,相互之间没有关联,对相互的成员也没有特殊访问权限
类模板的成员函数
类模板的成员函数与其他类相同,既可以定义在类内部,也可以定义在外部,定义在内部的成员函数隐式地声明为内联函数。
类模板的成员函数本身是一个普通函数,但类模板的每个实例都有自己版本的成员函数。因此类模板的成员函数具有和模板相同的模板参数。因此定义在类外的成员函数就必须以关键字 template 开始,后接模板参数列表:
template <typename T>
ret-type Blob<T>::member-name(param-list) {
...
}
如果一个成员函数没有被使用,则它不会被实例化。成员函数只有在被用到时才进行实例化。
这一特性使得某种类型不能完全符合模板操作的要求,我们仍能用该类型实例化类。
类模板内简化使用类名
当使用一个类模板类型时必须提供模板实参。但在类模板自己的作用域中,可以直接使用模板名而不必提供实参:
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];
}
// 返回类型不必写成 BlobPtr<T>&
BlobPtr& operator++();
BlobPtr& operator--();
private:
std::shared_ptr<std::vector<T>> check(std::size_t, const std::string&) const;
std::week_ptr<std::vector<T>> wptr;
std::size_t curr;
}
类模板外使用类名
类外的作用域中,直到遇到类名才表示进入类的作用域。所以在模板外使用成员时必须带上模板参数:
// 后置
template <typename T>
// BlobPtr:: 之前的作用域属于类外,因此返回类型需要带上模板实参
BlobPtr<T> BlobPtr<T>::operator++(int) {
// 这里属于类内作用域,ret的类型不必写成 BlobPtr<T>
BlobPtr ret = *this;
++*this;
return ret;
}
类模板和友元
当一个类包含一个友元声明,类与友元各自是否是模板是相互无关的。
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例;
如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友元
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==(const Blob<T>&, const Blob<T>&);
...
};
通用和特定的模板友元
template <typename T> class Pal;
class C {
// 用C实例化的Pal是C的一个友元
friend class Pal<C>;
// Pal2的所有实例都是C的友元,不需要前置声明(不需要事先声明Pal2)
template <typename T> friend class Pal2;
};
template <typename T> class C2 {
// 授权访问权限给同类型实例化的Pal类型
friend class Pal<T>;
// Pal2的所有实例都是C2的友元,不需要Pal2的前置声明
template <typename X> friend class Pal2;
// 非模板类Pal3是C2所有实例的友元;不需要Pal3的前置声明
friend class Pal3;
};
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
使类型参数成为友元
新标准中,可以将模板类型参数声明为友元:
template <typename T> class Bar {
friend Type; // 将访问权限授予用来实例化 Bar 的类型
...
};
Sales_data 将称为 Bar<Sales_data> 的友元
模板类型别名
使用typedef来引用实例化的类:
typedef Blob<string> StrBlob;
由于模板不是一个类型,我们不能定义一个typedef引用一个模板,即无法定义一个 typedef 引用 Blob
但是新标准允许我们为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // 相当于 pair<string, string> authors;
// 固定一个或多个模板参数:
template <typename T> using part = pair<T, unsigned>;
part<string> books; // pair<string, unsigned>
part<Vehicle> cars; // pair<Vehicle, unsigned>
part<Student> kids; // pair<Student, unsigned>
类模板的 static 成员
template <typename T> class Foo {
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};
// 每个static成员必须有且仅有一个定义
// 类模板的每个实例都有一个独有的static对象
template <typename T>
size_t Foo<T>::ctr = 0; // 定义并初始化 ctr
对任意给定的类型X,所有Foo
使用静态成员:
Foo<int> fi;
auto ct = Foo<int>::count();
ct = fi.count();
ct = Foo::count; // 错误,未指定使用哪个实例的count
模板声明
声明时的模板参数名字与定义时不必相同
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现在任何使用这些模板的代码之前
类模板的类型成员
对于普通类型,使用作用域运算符::来访问static成员和类型成员,而对于模板代码则存在歧义:
T::size_type* p;
编译器不会知道这是在定义指针p还是用变量 size_type 乘以名为 p 的变量。
默认情况下,C++假定通过作用域运算符访问的名字不是类型。如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。使用关键字 typename 来实现这一点:
template <typename T>
typename T::value_type top(const T& c) {
if (!c.empty()) {
return c.back();
} else {
// 如果容器 c 为空,则生成一个 T::value_type 值初始化的元素返回给调用者
return typename T::value_type();
}
}
当希望通知编译器一个名字表示类型时,必须使用关键字 typename, 而不是用 class
默认模板实参
// less<T> 是可调用对象类型
template <typename T, typename F = less<T>>
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;
}
bool i = compare(0, 42);
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn);
如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名后跟一对空的尖括号对:
template <typename T = int> class Numbers {
public:
Numbers(T v = 0): val(v) {}
private:
T val;
};
Numbers<long double> prec;
Numbers<> prec2; // 使用默认类型int
成员模板
一个类可以包含本身时模板的成员函数,无论这个类是不是类模板。这种成员被称为成员模板。成员模板不能是虚函数。
class DebugDelete {
public:
DebugDelete(std::ostream& s = std::cerr): os(s) {}
// 与函数类似,T的类型将由编译器推断
template <typename T> void operator()(T* p) const {
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::ostream& os;
};
// 使用
double* p = new double;
DebugDelete d;
d(p);
int* ip = new int;
DebugDelete()(ip);
类模板的成员模板
// 声明. b 和 e 是两个迭代器,用于拷贝两个迭代器之间的元素
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(std::make_shared<std::vector<T>>(b, e)) {}
// 使用. 编译器会推断出参数类型
int ia[] = {0,1,2,3,4,5};
vector<long> vi = {1,2,3,4,5,6};
list<const char*> w = {"now", "is", "the"};
Blob<int> a1(begin(ia), end(ia));
Blob<int> a2(vi.begin(), vi.end());
Blob<string> a3(w.begin(), w.end());
控制实例化
当模板被使用时才进行实例化,这意味着,如果相同的实例出现在多个对象文件。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件都有该模板的一个实例。
大系统中,多个文件实例化相同的模板会带来额外开销。
新标准中,可以通过显式实例化来避免多次实例化:
// 实例化声明和定义
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>的实例化会出现在其他文件
Blob<string> sa1, sa2;
// Blob<int>的实例在这里生成;接受initializer_list的构造函数也同时进行实例化
Blob<int> a1 = {0,1,2,3,4,5};
// 拷贝构造函数在这里实例化
Blob<int> a2(a1);
// compare的int版本的实例化在其他文件中生成
int i = compare(a1[0], a2[0]);
// templateBuild.cc
// 实例化文件,须为每个其他文件中声明为extern的类型和函数提供定义
template class Blob<string>;
template int compare(const int&, const int&);
// 本文件后续需要对上述声明进行定义
...
编译后,templateBuild.o会包含compare的int实例化版本的定义和Blob
链接时,将templateBuild.o 和 Application.o链接到一起。
对每个实例化声明,程序的某个位置必须有其显式的实例化定义
实例化定义会实例化所有成员
与类模板的普通实例化不同,编译器遇到一个实例化定义时,它不了解程序使用哪些成员函数,他会实例化所有成员,即使不使用某些成员。
类模板的实例化定义中,所用类型必须能用于所有成员函数
效率与灵活性
shared_ptr 在运行时才确定删除器类型,并且在reset时可能会改变其类型
unique_ptr 在声明时就需要传递删除器,所以其在编译器就确定了类型
模板实参推断
类型转换与模板类型参数
如果一个函数形参的类型使用了模板类型参数,那么它采用特殊的初始化规则。
只有很有限的集中类型转换会自动地应用于实参,通常编译器不是对实参进行类型转换,而是生成一个新的模板实例。
模板的类型转换:
- 顶层 const 在形参和实参中都会被忽略
- const 转换:可以将一个非 const 对象的引用或指针传递给 const 的引用或指针形参
- 数组或函数指针转换:如果函数形参不是引用,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为指向首元素的指针。类似的,函数实参可以转换为一个该函数类型的指针。
- 算术转换、派生类向基类的转换以及用户定义的转换都不能应用于函数模板
template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);
string s1("value1");
const string s2("value2");
fobj(s1, s2); // 拷贝,因此 s2 的 const 被忽略
fref(s1, s2); // 引用,全都转换为了 const
int a[10], b[42];
fobj(a, b); // 调用 f(int*, int*)
fref(a, b); // 错误,a是 int[10], b是 int[42]
// 虽然都是数组,但长度不一样,所以是不同的类型
// 如果形参是引用,则数组不会转换为指针
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有const转换和数组或函数到指针的转换
如果函数参数类型不是模板参数,则对实参进行正常的类型转换
显式函数模板实参
用户可以指定模板参数,有两种情况需要显式指定模板参数:
- 函数的返回类型
- 同一个模板参数的类型的多个函数参数类型不一致时,需要进行类型转换
1, 函数的返回类型是模板列表中的类型,但与函数参数类型都不相同,编译器无法推断其类型:
// 编译器无法推断T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
int i = 42; long lng = 3;
// 调用时指定返回类型:显式模板实参
auto val3 = sum<long long>(i, lng); // long long sum(int, long)
// 指定多个模板类型
auto val4 = sum<long long, int, long>(i, lng);
// 按顺序指定参数,若想要指定后面的类型,则必须指定前面的类型,不能跳过
// 因此好的设计是将返回类型放在模板参数列表的第一个位置
// 糟糕的设计:想要指定返回类型T3,需要先指定前面的类型T1和T2
template<typename T1, typename T2, typename T3>
T3 sum1(T1, T2);
auto val5 = sum1(long long, int, long)(i, lng); // long sum1(long long, int)
2, 类型不一致时,显式指定实参会发生类型转换
template <typename T>
bool compare(const T&, const T&);
long lng;
compare(lng, 1024); // 错误,模板参数不匹配
compare<long>(lng, 1024); // 正确:compare(long, long)
compare<int>(lng, 1024); // 正确:compare(int, int)
尾置返回类型与类型转换
当函数返回类型依赖于函数参数类型时,使用尾置返回类型:
// 尾置返回允许我们在参数列表之后声明返回类型
// decltype作用于解引用返回的是引用类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
...
return *beg;
}
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi", "bye"};
auto& i = fcn(vi.begin(), vi.end()); // fcn 返回 int&
auto& s = fcn(ca.begin(), ca.end()); // fcn 返回 string&
标准类型转换模板
有时我们不想要引用类型,而要使用元素本身的类型:
#include <type_traits>
// 使用 remove_reference 消除引用
// 由于type是模板类型,所以在声明时使用 typename 告知编译器 type 表示一个类型
template <typename It>
auto fcn(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
...
return *beg;
}
vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi", "bye"};
auto& i = fcn(vi.begin(), vi.end()); // fcn 返回 int
auto& s = fcn(ca.begin(), ca.end()); // fcn 返回 string
标准类型转换模板:
| Mod | T | Mode<T>::type |
|---|---|---|
| remove_reference | X& 或 X&& 否则 |
X T |
| add_const | X&、const X 或函数 否则 |
T const T |
| add_lvalue_reference | X& X&& 否则 |
TX 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 | 带符号类型 否则 |
unsignedX T |
| remove_extent | X[n] 否则 |
X T |
| remove_all_extents | X[n1][n2]... 否则 |
X T |
上述每个转换模板用法都与 remove_reference 用法类似
如果无法转换或没必要转换模板参数,则type成员就是模板参数类型本身
函数指针和实参推断
根据参数类型推断函数模板的实例:
template <typename T> int compare(const T&, const T&);
// pf1 指向实例 int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
重载情况下可能无法推断实例版本,需要显式模板实参消除歧义:
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
// 错误:无法确定实例版本
func(compare);
// 正确
func(compare<int>);
模板实参推断和引用
从左值引用推断类型:
template <typename T> void f1(T&);
template <typename T> void f2(const T&);
int i = 5; const int ci = 4;
f1(i); // T 是 int
f1(ci); // T 是 const int
f1(5); // 错误,5是右值
f2(i);
f2(ci);
f2(5); // 正确,const & 可以绑定到一个右值,T 是 int
从右值引用推断类型:
template <typename T> void f3(T&&);
f3(42);
引用折叠和右值引用参数
通常不能将右值引用绑定到一个左值上,但C++定义了两个例外规则,这两个例外规则是 move 这种标准库正常工作的基础:
- 右值引用参数推断规则:如果给右值引用的模板类型参数传递了一个左值,编译器推断模板类型参数为实参的左值引用类型
- 引用折叠规则:当间接创建了一个引用的引用时,引用会折叠成一个普通的左值引用类型(除了一个例外)
- 新标准中,只有一种特殊情况下才会折叠成右值引用:右值引用的右值引用
- 即:X& &, X& && 和 X&& & 折叠成 X&; X&& && 折叠成 X&&
- 不能直接创建引用的引用,有两种情况会间接创建:类型别名和模板参数
f3(i); // T 是 int&
f3(ci); // T 是 const int&
f(5); // T 是 int&&
如果一个函数参数是指向模板参数类型的右值引用(T&&),则可以传递给它任意类型的实参。并且其性质得到保持,包括const性质和左值右值属性。
如果将一个左值传递给这样的参数,则函数参数被实例化为一个普通左值引用(T&)。
右值引用参数的模板函数
当对右值引用参数传递左值时存在的问题:
template <typename T> void f3(T&& val) {
T t = val; // 拷贝还是绑定一个引用?
t = fcn(t); // 赋值只改变t还是既改变t又改变val?
if (val == t){ // 若T是引用类型,则一直为true
...
}
}
int i = 5;
f3(i); // T 是 int&,存在上述问题
f3(5); // T 是 int,不存在上述问题
实际上,右值引用通常用于两种情况:模板转发实参或模板被重载。
重载时:
template <typename T> void f(T&&); // 绑定到非 const 右值(可修改的右值)
template <typename T> void f(const T&); // 左值和 const 右值
理解std::move
std::move 是一个函数模板,接受任何类型的实参,返回所指对象的右值引用:
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi!"), s2;
s2 = std::move(string("bye!")); // string("bye!")是个临时值,右值
// 从这个右值移动给s2,move的模板类型参数T是 string&&,t发生引用折叠为 string&&
s2 = std::move(s1); // 赋值后,s1 的值是不确定的
// 从左值移动给s2,move的模板类型参数T是 string&,t发生引用折叠为 string&
虽然不能隐式地将一个左值转换为右值引用,但可以用 static_cast 显式地将一个左值转换为一个右值引用。
使用 static_cast 可能会截断左值(长类型转换为短类型时,比如 long->int, double->float)。
通过强制使用 static_cast,C++阻止了我们意外地进行隐式转换。
统一使用 std::move 使得我们在程序中查找潜在的截断左值的代码变得容易。
转发
头文件 utility中(move, forward)
有些函数调用了其他函数,需要将参数连同类型原封不动地转发给其他函数,这个过程中需要保持实参的所有性质,包括 const 和左值右值性质。
使用 std::forward 保持类型信息
通常,我们使用 forward 传递那些定义为模板类型参数的右值引用函数参数。通过返回类型上的引用折叠,forward 可以保持给定实参的左值右值属性:
#include <utility>
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 的参数是指向模板类型参数的右值引用,保证了参数传递性质的不变性。(const和左值右值属性)
函数f的参数不管是左值还是右值,是不是const,都能保持其性质不变
对于 forward 函数模板,编译器会推断函数参数的类型。
如果函数实参是个右值,forward
如果是左值,引用折叠后将返回 T&.
类似 std::move, std::forward 不使用 using 声明
重载与模板
函数模板可以被另一个模板或普通函数重载。
涉及到函数模板时,函数匹配规则会有受到的影响:
- 对于一个调用,所有模板实参推断成功的函数模板实例都会成为其候选函数
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板
- 与通常一样,可行函数按类型转换来排序
- 如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但如果有多个函数提供同样好的匹配,则:
- 如果这些函数中只有一个是非模板函数,则选择之
- 如果这些函数中没有非模板函数,有多个函数模板,而其中一个模板比其他模板更特例化,则选择之
- 否则,调用有歧义。
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解
// 打印任何我们不能处理的类型
template <typename T> string debug_rep(const T& t) {
ostringstream ret;
ret << t;
return ret.str();
}
// 指针版本
template <typename T> string debug_rep(T* p) {
ostringstream ret;
ret << "pointer: " << p;
if (p) {
ret << " " << debug_rep(*p);
} else {
ret << " null pointer";
}
return ret.str();
}
string s("hi");
cout << debug_rep(s) << endl; // 匹配第一个 string debug_rep(const string&)
cout << debug_rep(&s) << endl; // 匹配第二个 string debug_rep(string*)
const string* sp = &s;
cout << debug_rep(sp) << endl; // 匹配第二个 string debug_rep(const string*)
// 两个都精确匹配,但第一个更通用,第二个是特例化版本
可变参数模板
可变数目的参数被称为参数包,模板中的参数叫模板参数包,函数的参数叫函数参数包。
- class... 或 typename... 指出接下来的参数表示0个或多个类型的列表;
- 一个类型名后跟一个省略号表示0个或多个非类型参数的列表。
- 函数参数列表中,如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
编译器从参数的实参推断模板参数类型。对于可变参数模板,编译器还会推断包中参数数目:
template <typename T, typename... Args>
void foo(const T& t, const Args& ... rest);
int i = 0; double d = 3.14; string s = "red firefox";
foo(i, s, 42, d); // 包中有三个参数
// void foo(const int&, const string&, const int&, const double&);
foo(s, 42, "hi"); // void foo(const string&, const int&, const char[3]&);
foo(d, s); // void foo(const double&, const string&);
foo("hi"); // 空包。void foo(const char[3]&);
sizeof...运算符
获取参数数目,与sizeof函数类似,返回一个常量表达式:
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // 类型参数数目
cout << sizeof...(args) << endl; // 函数参数数目
}
编写可变参数函数模板
// 可变参数函数通常是递归的,每次处理一个参数
template <typename T>
ostream& print(ostream& os, const T& t) {
return os << t;
}
template <typename T, typename... Args>
ostream& print(ostream& os, const T& t, const Args&... rest) {
os << t << " ";
return print(os, rest...);
}
每个省略号就是一个扩展,省略号前的叫做模式。
扩展一个包就是将包分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
扩展时要提供用于每个扩展元素的模式。
更复杂的扩展模式:
// 在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(os, debug_rep(rest...));
// 则等价于:(编译错误)
print(os, debug_rep(a1, a2, ..., an));
扩展中的模式会独立地应用于包中的每个元素
转发参数包
template <class... Args> inline
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc(); // 保证有足够的空间
alloc.construct(first_free++, std::forward<Args>(args)...);
}
svec.emplace_back(10, 'c');
// 在 construct 的调用中扩展出:
std::forward<int>(10), std::forward<char>('c')
// 传递右值,调用移动构造函数:
svec.emplace_back(s1 + s2); // s1 + s2 得到 string 类型的临时量
// std::forward<string>(string("helloword")) 的结果类型是 string&&
// construct 会将此右值传递给 string 的移动构造函数来创建新元素
可变参数函数通常将他们的参数转发给其他函数,其形式基本类似:
template <typename... Args>
void fun(Args&&... args) {
// work 的实参既扩展 Args 又扩展 args
work(std::forward<Args>(args)...);
}
模板特例化
函数重载与特例化
编写通用模板使之对任何可能的模板实参都能实例化,并不总是能办到。有些时候需要编写特例化模板,利用某些特定知识来编写更高效的代码。
例,比较两个大小的函数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 = "hello", * p2 = "world";
compare(p1, p2); // 调用第一个
compare("hello", "world"); // 调用第二个
传入两个字符串字面量参数的情况,字符串字面量是不可变字符数组,最匹配的是第二个版本。当他们转换为指针时,其类型是 const char* const&,第一个版本可行但不能精确匹配。因此需要重载或特例化。
接受字符数组参数的版本最匹配,但如果向要将上述字面量数组转换为指针,可定义如下特例化版本:
// 特例化,而不是重载:
// compare的一个特殊版本,处理字符数组的指针
// 如果想要定义一个特例化版本,函数参数类型必须与一个先前声明的模板相匹配。即要事先声明通用版本
template <> int compare(const char* const& p1, const char* const& p2) {
return strcmp(p1, p2);
}
为了特例化一个模板,原模板的声明必须在作用域中(特例化模板声明时可见)。而且在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。
对于普通类和函数,丢失声明的情况很容易发现,但如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码,从而产生错误,而这种错误很难查找。
模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化
例:将自定义类 Sales_data 的对象保存在无序容器中,无序容器使用 hash<key_type>. 需要为 hash 模板定义特例化版本。要求:
- 一个重载的调用运算符,接受一个容器关键字类型的对象,返回一个 size_t
- 两个类型成员,result_type 和 argument_type,本别调用运算符的返回类型和参数类型
- 默认构造函数和拷贝赋值运算符
// 需要在原模板定义所在的命名空间中特例化它,打开std命名空间:
namespace std {
template <>
struct hash<Sales_data> {
typedef size_t result_type;
typedef Sales_data argument_type;
size_t operator()(const Sales_data&) const;
};
size_t hash<Sales_data>::operator()(const Sales_data& s) const {
// 将如何定义一个好的哈希函数的复杂任务交给了标准库;对三个hash值进行“与”位运算
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // 关闭std命名空间;注意没有分号
// Sales_data.h:(hash<Sales_data>使用了Sales_data的私有成员,利用友元授权)
template <class T> class std::hash;
class Sales_data {
friend class std::hash<Sales_data>;
...
};
// 编译器会调用该特例化版本,使用 hash<Sales_data> 和 Sales_data 的 operator==
unordered_multiset<Sales_data> sset;
类模板部分特例化
与函数模板不同,类模板的特例化不必为所有模板参数提供实参。可以只指定一部分模板参数,或参数的一部分特性。一个类模板的部分特例化本身是一个模板。
// 标准库的 remove_reference 是通过一系列特例化版本来完成其功能的:
// 原模板
template <class T> struct remove_reference {
typedef T type;
};
// 部分特例化版本,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> {
typedef T type;
};
template <class T> struct remove_reference<T&&> {
typedef T type;
};
int i;
remove_reference<decltype(42)>::type a; // decltype(42) 是 int
remove_reference<decltype(i)>::type b; // decltype(i) 是 int&,使用了左值引用版本
remove_reference<decltype(std::move(i))>::type c; // decltype(i) 是 int&&,使用了右值引用版本
特例化成员而不是类
template <typename T> struct Foo {
Foo(const T& t = T()): mem(t) {}
void Bar() { ... }
T mem;
};
template <> // 特例化一个模板
void Foo<int>::Bar() { // 特例化一个成员
...
}
小结
模板是编译器用来生成特定类型或函数的蓝图,生成特定类型或函数的过程称为实例化。
标准库算法都是函数模板,标准库容器都是类模板。
显式模板实参允许我们固定一个或多个模板参数的类型或值。对于指定了显式模板实参的模板参数,可以应用正常的类型转换。
模板特例化就是将一个或多个模板参数绑定到特定类型或值上。当我们不能(或不希望)将模板定义用于某些特定类型时,特例化非常有用。
可变参数模板可以接受数目和类型可变的参数。它允许我们编写像容器的 emplace 成员和标准库 make_shared 这样的函数,实现将实参传递给对象的构造函数。

浙公网安备 33010602011771号