模板与泛型编程
模板是创建类或函数的蓝图或者说公式,需要提供足够的信息,将蓝图转换为特定的类或函数。这种转换发生在编译时。
16.1定义模板
16.1.1函数模板
// 模板参数列表不能为空
template<typename T>
int compare(const T &v1, const T &v2) {
// 只使用<运算符降低了compare函数对要处理的类型的要求。这些类型
// 必须支持<,但不必同时支持>。
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
实例化函数模板
// 实例化出int compare(const int &, const int &)
cout << compare(1, 0) << endl;
vector<int> vec1{1, 2, 3};
vector<int> vec2{4, 5, 6};
// 实例化出int compare(const vector<int> &, const vector<int> &)
cout << compare(vec1, vec2) << endl; // T为vector<int>
编译器生成的版本通常被称为模板的实例。
模板类型参数
可以将类型参数看作类型说明符、指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换:
// typename可以用class代替
template<typename T>
T foo(T *p) {
T tmp = *p; // temp的类型将是指针p指向的类型
// ...
return tmp;
}
在模板参数列表中,
typename
和class
没有什么不同,只是typename
是后面才引入的。
非类型模板参数
通过一个特定的类型名而非关键字
class
或typename
来指定非类型参数。
// 处理字符串字面常量,这种字面常量是const char的数组,由于不能拷贝一个数组,所以将
// 自己的参数定义为数组的引用。由于希望能比较不同长度的字符串字面常量,因此为模板定义
// 了两个非类型的参数。
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M]) {
return strcmp(p1, p2);
}
// 会使用字面常量的大小来代替N和M,需要注意的是末尾的空字符。
// 实例化出int compare(const char (&p1)[3], const char (&p2)[4])
compare("hi", "mom");
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或(左值)引用。绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。
在模板定义内,模板非类型参数是一个常量值。在需要常量表达式的地方,可以使用非类型参数,例如,指定数组大小。
编写类型无关的代码
// 函数参数设定为const的引用保证了函数可以用于不能拷贝的类型;
// 使用less即使用于指针也能正确运行。
template<typename T>
int compare(const T &v1, const T &v2) {
// 原始版本的问题是,如果两个指针未指向相同的数组,则代码的行为是未定义的。
// 虽然less<T>的默认实现用的也是<。
if (less<T>()(v1, v2)) return -1;
if (less<T>()(v2, v1)) return 1;
return 0;
}
模板程序应该尽量减少对实参类型的要求。
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当实例化出(使用)模板的一个特定版本时,编译器才会生成代码,这一特性影响了如何组织代码以及错误何时被检测到。
通常,当调用一个函数时,编译器只需要掌握函数的声明。类似的,当使用一个类类型的对象时,类定义必须是可用的,但成员函数的定义不必已经出现。因此,将类定义和函数声明放在头文件中,而普通函数和类的成员函数的定义放在源文件中。
模板则不同:为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此,与非模板代码不同,模板的头文件通常既包括声明也包括定义。
函数模板和类模板成员函数的定义通常放在头文件中。
模板包含两种名字:
- 那些不依赖于模板参数的名字。
- 那些依赖于模板参数的名字。
当使用模板时,所有不依赖于模板参数的名字都必须是可见的,这是由模板的提供者来保证的。而且,模板的提供者必须保证,当模板被实例化时,模板的定义,包括类模板的成员的定义,也必须是可见的。
用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的,这是由模板的用户来保证的。
模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明。模板的用户必须包含模板的头文件,以及用来实例化模板的任何类型的头文件。
16.1.2类模板
定义类模板
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;
};
// 使用相同的特定类型版本的Blob(即Blob<int>)
Blob<int> ia; // 空Blob<int>
Blob<int> ia2 = {0, 1, 2, 3, 4}; // 有5个元素的Blob<int>
// 下面的定义实例化出两个不同的Blob类型
Blob<string> names; // 保存string的Blob
Blob<double> prices; // 不同的元素类型
一个类模板的每个实例都形成一个独立的类,彼此之间没有关联,也不会对任何其他类型的成员有特殊访问权限。
类模板的成员函数
类模板的每个实例都有其自己版本的成员函数,因此,类模板的成员函数具有和模板相同的模板参数。所以,定义在类模板之外的成员函数就必须以关键字
template
开始,后接类模板参数列表。
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);
}
}
template<typename T>
T &Blob<T>::back() {
check(0, "back on empty Blob");
return data->back();
}
template<typename T>
T &Blob<T>::operator[](size_type i) {
// 如果i太大,check会抛出异常,阻止访问一个不存在的元素。
check(i, "subscript out of range");
return (*data)[i];
}
template<typename T>
void Blob<T>::pop_back() {
check(0, "pop_back on empty Blob");
data->pop_back();
}
template<typename T>
Blob<T>::Blob()
: data(std::make_shared<std::vector<T>>()) {
}
template<typename T>
Blob<T>::Blob(std::initializer_list<T> il)
: data(std::make_shared<vector<T>>(il)) {
}
类模板成员函数的实例化
默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
在类代码内简化模板类名的使用
// 若试图访问一个不存在的元素,BlobPtr抛出一个异常。
template<typename T>
class BlobPtr {
public:
BlobPtr() : curr(0) {}
BlobPtr(Blob<T> &a, std::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&,而不是BlobPtr<T>&。
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; // 数组中的当前位置
};
当处于一个类模板的作用域中时,编译器处理模板自身引用时就好像已经提供了与模板参数匹配的实参一样。
在类模板外使用类模板名
当在类模板外定义其成员时,必须记住,此时并不在类的作用域中,直到遇到类名才表示进入类的作用域:
// 后置:递增/递减对象但返回原值。
template<typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) {
// 此处无须检查,调用前置递增时会进行检查。
BlobPtr ret = *this; // 保存当前值
++*this; // 推进一个元素,前置++检查递增是否合法。
return ret;
}
类模板和友元
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板的实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
一对一友好关系
类模板与另一个(类或函数)模板间友好关系的最常见的形式是建立对应实例及其友元间的友好关系。
为了引用(类或函数)模板的一个特定实例,必须首先声明模板自身。一个模板声明包括模板参数列表:
// 前置声明,在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> &);
// 其他成员保持一致
};
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自己的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到。
template<typename T>
class Pal;
// C是一个普通的非模板类
class C {
friend class Pal<C>; // 用类C实例化的Pal是C的一个友元
// Pal2的所有实例都是C的友元,这种情况无须前置声明。
template<typename T> friend class Pal2;
};
// C2本身是一个类模板
template<typename T>
class 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 {
// 内置类型也是允许的,以便能用内置类型来实例化Bar这样的类。
friend Type; // 将访问权限授予用来实例化Bar的类型
// ...
};
模板类型别名
类模板的一个实例定义了一个类类型,因此可以定义一个
typedef
来引用实例化的类:
// 无法定义一个typedef引用Blob<T>
typedef Blob<string> StrBlob;
由于模板不是一个类型,不能定义一个
typedef
引用一个模板。
但是新标准允许为类模板定义一个类型别名:
template<typename T> using twin = pair<T, T>;
twin<string> authors; // authors是一个pair<string, string>
一个模板类型别是一族类的别名:
twin<int> win_loss; // win_loss是一个pair<int, int>
twin<double> area; // area是一个pair<double, double>
当定义一个模板类型别名时,可以固定一个或多个模板参数:
template<typename T> using partNo = pair<T, unsigned>;
partNo<string> books; // books是一个pair<string, unsigned>
partNo<Vehicle> cars; // cars是一个pair<Vehicle, unsigned>
partNo<Student> kids; // 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成员Foo<string>::ctr和Foo<string>::count
Foo<string> fs;
// 所有三个对象共享相同的Foo<int>::ctr和Foo<int>::count成员
Foo<int> fi, fi2, fi3;
与任何其他
static
数据成员相同,模板类的每个static
数据成员必须有且仅有一个定义。但是,类模板的每个实例都有一个独有的static
对象。因此,将static
数据成员也定义为模板:
template<typename T>
size_t Foo<T>::ctr = 0; // 定义并初始化ctr
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?
16.1.3模板参数
模板参数与作用域
一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。模板参数会隐藏外层作用域中声明的相同名字,但是,在模板内不能重用模板参数名。因此,一个模板参数名在一个特定模板参数列表中只能出现一次。
typedef double A;
template<typename A, typename B>
void f(A a, B b) {
A tmp = a; // tmp的类型为模板参数A的类型,而非double。
double B; // 错误:重声明模板参数B。
}
模板声明
模板声明必须包含模板参数:
// 声明但不定义compare和Blob
template<typename T> int compare(const T &, const T &);
template<typename T> class Blob;
一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
使用类的类型成员
在普通(非模板)代码中,编译器掌握类的定义。因此,它知道通过作用域运算符访问的名字是类型还是
static
成员。
但对于模板代码来说就存在困难。例如,当编译器遇到类似T::mem
这样的代码时,它不会知道mem
是一个类型成员还是一个static
数据成员,直至实例化时才会知道。但是,为了处理模板,编译器必须知道名字是否表示一个类型。
// 是正在定义一个名为p的变量还是将一个名为size_type的static
// 数据成员与名为p的变量相乘?
T::size_type *p;
默认情况下,c++假定通过作用域运算符访问的名字不是类型。因此,如果希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。通过使用
typename
(此时不能使用class
)来实现这一点:
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()
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;
}
// T为int,可调用对象的类型为less<int>,将使用其进行比较操作。
bool i = compare(0, 42);
Sales_data item1(cin), item2(cin);
// T为Sales_data,F被推断为compareIsbn的类型。
bool j = compare(item1, item2, compareIsbn);
模板默认实参与类模板
无论何时使用一个类模板,都必须在模板名之后接上尖括号,指出类必须从一个模板实例化而来。
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成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。这种成员被称为成员模板,其不能是虚函数。
普通(非模板)类的成员模板
// 函数对象类,对给定指针执行delete。
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; // 可像delete表达式一样使用的对象
d(p); // 调用DebugDelete::operator()(double*),释放p。
int *ip = new int;
// 在一个临时DebugDelete对象上调用operator()(int*)
DebugDelete()(ip);
// 销毁p指向的对象
// 实例化DebugDelete::operator()<int>(int *)
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// 销毁sp指向的对象
// 实例化DebugDelete::operator()<string>(string *)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
// unique_ptr的析构函数会调用DebugDelete的调用运算符,因此,无论何时unique_ptr
// 的析构函数实例化时,DebugDelete的调用运算符都会实例化。
类模板的成员模板
对于类模板,也可以为其定义成员模板。在此情况下,类和成员各自有自己的、独立的模板参数。
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, 6, 7, 8, 9};
vector<long> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
list<const char *> w = {"now", "is", "the", "time"};
// 实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> a1(begin(ia), end(ia));
// 实例化Blob<int>类的接受两个vector<long>::iterator的构造函数
Blob<int> a2(vi.begin(), vi.end());
// 实例化Blob<string>及其接受两个list<const char*>::iterator参数的构造函数
Blob<string> a3(w.begin(), w.end());
16.1.5控制实例化
由于当模板被使用时才会进行实例化,因此,相同的实例可能出现在多个对象文件中。当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在新标准中,可以通过显式实例化来避免这种开销:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
// 实例化声明与定义
extern template class Blob<string>; // 声明
template int compare(const int &, const int &); // 定义
当编译器遇到
extern
模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为extern
就表示承诺在程序其他位置有该实例化的一个非extern
声明(定义)。对于一个给定的实例化版本,可能有多个extern
声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此extern
声明必须出现在任何使用此实例化版本的代码之前:
// Application.cc,必须和templateBuild.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, 5, 6, 7, 8, 9};
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>; // 实例化类模板的所有成员
当编译器遇到一个实例化定义时,它为其生成代码。对每个实例化声明,在程序中某个位置必须有其显式的实例化定义。
实例化定义会实例化所有成员
即使不使用某个成员,它也会被实例化。因此,用来显式实例化一个类模板的类型,必须能用于模板的所有成员。
16.2模板实参推断
16.2.1类型转换与模板类型参数
顶层
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(string, string),顶层const被忽略。
fobj(s1, s2);
// 调用fref(const string &, const string &),将s1转换为const是允许的。
fref(s1, s2);
int a[10], b[42];
// 调用f(int *, int *)
fobj(a, b);
// 由于形参是一个引用,因此数组不会自动转换为指针,而实参中
// 两个数组的大小不同,因此是不同的类型。
fref(a, b); // 错误:数组类型不匹配。
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有
const
转换及数组或函数到指针的转换。
使用相同模板参数类型的函数形参
一个模板类型参数可以用作多个函数形参的类型。由于只允许有限的几种类型转换,因此传递给这些形参的实参必须具有相同的类型。如果推断出的类型不匹配,则调用就是错误的。
long lng;
compare(lng, 1024); // 错误:不能实例化compare(long, int)。
如果希望允许对函数实参进行正常的类型转换,可以将函数模板定义为两个类型参数。
正常类型转换应用于普通函数实参
函数模板可以有那些用普通类型定义的参数,即,不涉及模板类型参数的类型。这种函数实参不进行特殊处理:它们正常转换为对应形参的类型:
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函数模板显式实参
某些情况下,编译器无法推断出模板实参的类型。其他一些情况下,希望允许用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,这两种情况最长出现。
指定显式模板实参
// 希望用户指定结果的类型,从而选择合适的精度。
// 编译器无法推断T1,它未出现在函数参数列表中。
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)
显式模板实参按由左至右的顺序与对应的模板参数匹配。只有尾部(最右)参数的显式模板实参才可以忽略,而且前提是它们可以从函数参数推断出来。
// 糟糕的设计:用户必须指定所有三个模板参数,原因在于T3是返回类型,无法从函数实参推断而来。
template<typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
// 总是必须为所有三个形参指定实参。
// 错误:不能推断前几个模板参数。
auto val3 = alternative_sum<long long>(i, lng);
// 正确:显式指定了所有三个参数。
auto val2 = alternative_sum<long long, int, long>(i, lng);
正常类型转换应用于显式指定的实参
对于模板类型参数已经显式指定了的函数实参,也进行正常的类型转换:
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>
??? &fcn(It beg, It end) {
// 处理序列
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&
// 知道函数应该返回*beg,而且知道可以用decltype(*beg)来获取此表达式的类型。但是,
// 在编译器遇到函数的参数列表之前,beg都是不存在的。为了定义此函数,必须使用尾置
// 返回类型。由于尾置返回出现在参数列表之后,它可以使用函数的参数:
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
// 处理序列
return *beg; // 返回序列中一个元素的引用
}
进行类型转换的标准库模板类
有时,无法直接获得所需要的类型。例如,可能希望编写一个类似
fcn
的函数,但返回一个元素的值而非引用。
在编写这个函数的过程中,会面临一个问题:对于传递的参数的类型,几乎一无所知。在此函数中,知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不会生成元素,只能生成元素的引用。
为了获得元素类型,可以使用标准库的类型转换模板(定义在头文件type_traits
中)。
// 使用remove_reference来获得元素类型,其有一个模板类型参数和一个名为type的(public)
// 类型成员。如果用一个引用类型实例化remove_reference,则type将表示被引用的类型。
// 注意,type是一个类的成员,而该类依赖于一个模板参数。因此,必须在返回类型的声明中
// 使用typename来告知编译器,type表示一个类型。
template<typename It>
auto fcn2(It beg, It end) -> typename remove_reference<decltype(*beg)>::type {
// 处理序列
return *beg; // 返回序列中一个元素的拷贝
}
16.2.4函数指针和实参推断
当用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。如果不能从函数指针类型确定模板实参,则产生错误:
template<typename T>
int compare(const T &, const T &);
// pf1指向实例int compare(const int &, const int &)
int (*pf1)(const int &, const int &) = compare;
// func的重载版本,每个版本接受一个不同的函数指针类型。
void func(int(*)(const string &, const string &));
void func(int(*)(const int &, const int &));
// 错误:使用compare的哪个实例?对func的调用既可以实例化接受int的版本,
// 也可以实例化接受string的版本。
func(compare);
// 可以通过使用显式模板实参来消除func调用的歧义:
// 正确:显式指出实例化哪个compare版本。
func(compare<int>); // 传递compare(const int &, const int &)
16.2.5模板实参推断和引用
从左值引用函数参数推断类型
当一个函数参数的模板类型参数是一个普通(左值)引用时(
T&
),绑定规则使得只能传递给它一个左值:
template<typename T>
void f1(T &); // 实参必须是一个左值
// 对f1的调用使用实参所引用的类型作为模板参数类型
f1(i); // i是一个int,模板参数类型T是int。
f1(ci); // ci是一个const int,模板参数T是const int。
f1(5); // 错误:传递给一个&参数的实参必须是一个左值。
如果一个函数参数的类型是
const T&
时,此时可以传递给它任何类型的实参:一个对象(const
或非const
)、一个临时对象或是一个字面常量值。由于const
已经是函数参数类型的一部分,因此,它不会也是模板参数类型的一部分:
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。
从右值引用函数参数推断类型
当一个函数参数是一个右值引用(
T&&
)时,可以传递给它一个右值:
template<typename T>
void f3(T &&);
f3(42); // 实参是一个int类型的右值,模板参数T是int。
引用折叠和右值引用参数
C++在正常绑定规则之外定义了两个例外规则:
- 当一个左值传递给函数的右值引用参数,且此右值引用指向模板类型参数(
T&&
)时,编译器推断模板类型参数为实参的左值引用类型。- 如果间接创建一个引用的引用,则这些引用形成了折叠。在所有情况下(除了一个例外),引用会折叠成一个普通的左值引用类型。在新标准中,右值引用的右值引用才会折叠成右值引用:
X& &
、X& &&
和X&& &
都折叠成类型X&
。- 类型
X&& &&
折叠成X&&
。
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数。
// 等价于void f3<int&>(int& &&)(一个右值引用,指向的是左值引用),
// 从右到左,折叠为void f3<int&>(int&)。
f3(i); // 实参是一个左值,模板参数T是int&。
f3(ci); // 实参是一个左值,模板参数T是const int&。
因此,可以将任意类型的实参传递给
T&&
类型的函数参数。
编写接受右值引用参数的模板函数
// 当传递一个右值,例如字面常量42,T为int,此时赋值不会改变val;
// 当传递一个左值,例如int变量i,T为int&,此时赋值会改变val。
template<typename T>
void f3(T &&val) {
T t = val; // 拷贝还是绑定一个引用?
t = fcn(t); // 赋值只改变t还是既改变t又改变val?
if (val == t) { /* ... */ } // 若T是引用类型,则一直为true。
}
当代码中涉及的类型可能是普通类型,也可能是引用类型时,编写正确的代码就变得异常困难。在实际中,右值引用通常用于两种情况:模板转发其实参或模板被重载。
目前应该注意的是,使用右值引用的函数模板通常应该使用以下方式进行重载:
// 绑定到非const右值,即可修改的右值。
template<typename T> void f(T &&);
// 左值和const右值
template<typename T> void f(const T &);
16.2.6理解std::move
template<typename T>
// 函数参数T&&通过引用折叠可以与任何类型的实参匹配
typename remove_reference<T>::type &&move(T &&t) {
return static_cast<typename remove_reference<T>::type &&>(t);
}
从一个左值static_cast到一个右值引用是允许的
虽然不能隐式地将一个左值转换为右值引用,但可以用
static_cast
显式地(避免意外转换)将一个左值转换为一个右值引用。不过更推荐使用move
函数。
16.2.7转发
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在这种情况下,需要保持被转发实参的所有性质,包括实参类型是否是
const
的以及实参是左值还是右值。
// 接受一个可调用对象和另外两个参数的模板。
// 对"翻转"的参数调用给定的可调用对象。
// flip1是一个不完整的实现:顶层const和引用丢失了。
template<typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2) {
f(t2, t1);
}
// 正常情况下工作得很好,但当希望用它调用一个接受引用参数的函数时就会出现问题:
void f(int v1, int &v2) { // 注意v2是一个引用
cout << v1 << " " << ++v2 << endl;
}
f(42, i); // f改变了实参i
flip1(f, j, 42); // 通过flip1调用f不会改变j
// 问题在于j被传递给flip1的参数t1,此参数是一个普通的、非引用的类型int,而非int&,
// 因此会进行值拷贝操作,所以f改变的是t1,而非j。
// 重写函数,使其参数能保持给定实参的左值性,同时保持参数的const属性。
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2) {
f(t2, t1);
}
// 将传递给参数t1一个左值j,推断出T1的类型为int&,最后折叠为int&。由于是引用类型,
// t1被绑定到j上,因此可以改变j的值。
flip2(f, j, 42);
// 但是这个版本不能用于接受右值引用的参数的函数:
void g(int &&i, int &j) {
cout << i << " " << j << endl;
}
// 参数t2将被传递给g的右值引用参数,即使传递一个右值给flip2,传递给g的将是flip2
// 中名为t2的参数。函数参数与其他任何变量一样,都是左值表达式。因此,flip2中对g
// 的调用将传递给g的右值引用参数一个左值,这是错误的。
flip2(g, i, 42);
如果一个函数参数是指向模板类型参数的右值引用(如
T&&
),它对应的实参的const
属性和左值/右值属性将得到保持。
在调用中使用std::forward保持类型信息
可以使用
forward
(定义在头文件utility
中)来保持原始实参的类型,必须通过显式模板实参来调用,返回该显式实参类型的右值引用。
通常情况下,使用forward
传递那些定义为模板类型参数的右值引用的函数参数。通过其返回类型上的引用折叠,forward
可以保持给定实参的左值/右值属性:
// 由于arg是一个模板类型参数的右值引用,Type将表示传递给arg实参的所有类型信息。如果实参
// 是一个右值,则Type是一个普通(非引用)类型,forward<Type>将返回Type&&。如果实参是
// 一个左值,则通过引用折叠,Type本身是一个左值引用类型。在此情况下,返回类型是一个指向
// 左值引用类型的右值引用。再次对forward<Type>的返回类型进行引用折叠,将返回一个左值
// 引用类型。
template<typename Type> intermediary(Type &&arg) {
finalFcn(std::forward<Type>(arg));
// ...
}
template<typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2) {
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
// i将以int&类型传递给g,42将以int&&类型传递给g。
flip(g, i, 42);
16.3重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载,此时,函数匹配规则会受到一些影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
- 否则,此调用有歧义。
编写重载模板
// 调试函数的最通用版本
template<typename T> string debug_rep(const T &t) {
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str(); // 返回ret绑定的string的一个副本
}
// 调试函数打印指针的版本,需要注意的是,此函数不能用于打印字符指针,因为IO库
// 为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 string*&),T被绑定到string*。
// debug_rep(string *),T被绑定到string。
// 第二个版本是此调用的精确匹配,第一个版本的实例需要进行普通指针到const指针的转换。
cout << debug_rep(&s) << endl;
多个可行模板
// 两个模板都是可行的,而且两个都是精确匹配:
// debug_rep(const string*&),T被绑定到string*。
// debug_rep(const string *),T被绑定到const string。
// 但是,根据重载函数模板的特殊规则,会选择第二个版本,即,更特例化的版本。设计这条
// 规则的原因是,没有它,将无法对一个const的指针调用指针版本的debug_rep。问题在于
// 模板debug_rep(const T&)本质上可以用于任何类型,包括指针类型。此模板更通用,后者
// 只能用于指针类型。没有这条规则,传递const的指针的调用永远是有歧义的。
const string *sp = &s;
cout << debug_rep(sp) << endl;
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
非模板和模板重载
// 调试函数的非模板版本
string debug_rep(const string &s) {
return '"' + s + '"';
}
// 有两个同样好的可行函数:
// debug_rep<string>(const string&),第一个模板,T被绑定到string。
// debug_rep(const string&),普通非模板函数。
// 此时会选择非模板版本。
string s("hi");
cout << debug_rep(s) << endl;
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
重载模板与类型转换
// 考虑c风格字符串指针和字符串字面常量的情况。
// 此时,三个版本都是可行的:
// debug_rep(const T&),T被绑定到char[10]。
// debug_rep(T*),T被绑定到const char。
// debug_rep(const string&),要求从const char*到string的类型转换。
// 两个模板都提供精确匹配,第二个模板需要进行一次(许可的)数组到指针的转换,而对于函数匹配
// 来说,这种转换被认为是精确匹配。非模板版本是可行的,但需要进行一次用户定义的类型转换,
// 因此它没有精确匹配那么好,所以两个模板成为可能调用的函数。由于T*版本更加特例化,编译器会
// 选择它。
cout << debug_rep("hi world!") << endl;
// 如果希望将字符指针按string处理,可以定义另外两个非模板重载版本:
string debug_rep(char *p) {
return debug_rep(string(p));
}
string debug_rep(const char *p) {
return debug_rep(string(p));
}
缺少声明可能导致程序行为异常
// 为了使char*版本的debug_rep正确工作,在定义此版本时,debug_rep(const string&)
// 的声明必须在作用域中。否则,就可能调用错误的版本:
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实例化为string的版本。
return debug_rep(string(p));
}
通常,如果使用了一个忘记声明的函数,代码将编译失败。但对于重载函数模板的函数而言,则不是这样。如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。
因此,在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器由于未遇到希望调用的函数而实例化一个并非所需的版本。
16.4可变参数模板
一个可变参数模板就是一个接受可变数目参数的模板函数或模板类。可变数目的参数被称为参数包。存在两种参数包:模板参数包,表示零个或多个模板参数;函数参数包,表示零个或多个函数参数。
// 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";
// void foo(const int&, const string&, const int&, const double&)
foo(i, s, 42, d);
// void foo(const string&, const int&, const char[3]&)
foo(s, 42, "hi");
// void foo(const double&, const string&)
foo(d, s);
// void foo(const char[3]&)
foo("hi");
sizeof…运算符
当需要知道包中有多少元素时,可以使用
sizeof...
运算符,它也返回一个常量表达式,而且不会对其实参求值:
template<typename ...Args>
void g(Args ...args) {
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}
16.4.1编写可变参数函数模板
可以使用一个
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 << ", "; // 打印第一个实参
// 此调用只传递了两个实参,其结果是rest中的第一个实参被绑定到t,剩余实参形成下一个
// print调用的参数包。因此,在每个调用中,包中的第一个实参被移除,成为绑定到t的实参。
return print(os, rest...); // 递归调用,打印其他实参。
}
16.4.2包扩展
对于一个参数包,除了获取其大小外,能对它做的唯一的事情就是扩展它。当扩展一个包时,还要提供用于每个扩展元素的模式。
扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过在模式右边放一个省略号(...
)来触发扩展操作。
template<typename T, typename ...Args>
// 扩展Args,将模式const Args&应用到模板参数包Args中的每个元素。因此,
// 此模式的扩展结果是一个逗号分隔的零个或多个类型的列表,每个类型都形如
// const type&。
ostream &print(ostream &os, const T &t, const Args&... rest) {
os << t << ", ";
// 扩展rest,模式是函数参数包的名字,此模式扩展出一个由包中元素组成的、
// 逗号分隔的列表。下面的调用等价于:
// print(os, s, 42);
return print(os, rest...);
}
// 包中有两个参数
print(cout, i, s, 42);
// 最后两个实参的类型和模式一起确定了尾置参数的类型。此调用被实例化为:
print(ostream&, const int&, const string&, const int&);
理解包扩展
C++还允许更复杂的扩展模式:
// 在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)...);
}
errorMsg(cerr, fcnName, code.num(), otherData, "other", item);
// 等价于:
print(cerr, debug_rep(fcnName), debug_rep(code.num()),
debug_rep(otherData), debug_rep("other"),
debug_rep(item));
// 与之相对,下面的模式会编译失败:
// print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误:此调用无匹配函数。
扩展中的模式会独立地应用于包中的每个元素。
16.4.3转发参数包
在新标准下,可以组合使用可变参数模板与
forward
机制来编写函数,实现将其实参不变地传递给其他函数。
// 为StrVec类添加一个emplace_back成员,类似于标准库容器,其是一个可变参数成员模板,
// 它用实参在容器管理的内存空间中直接构造一个元素。
class StrVec {
public:
template<typename ...Args> void emplace_back(Args&&...);
// 其他成员保持一致
}
template<typename ...Args>
inline
// 因为string有多个构造函数,参数各不相同,由于希望使用string的移动构造函数,因此还需要
// 保持传递给emplace_back的实参的所有类型信息,保持类型信息是一个两阶段的过程。首先,
// 为了保持实参中的类型信息,必须将函数参数定义为模板类型参数的右值引用;其次,必须
// 使用forward来保持实参的原始类型。
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc(); // 如果需要的话重新分配StrVec内存空间
alloc.construct(first_free++, std::forward<Args>(args)...);
}
// construct调用中的模式会扩展出:
// std::forward<int>(10), std::forward<char>(c)
svec.emplace_back(10, 'c');
// 通过在调用中使用forward,保证如果调用emplace_back时传递的是一个右值,
// 则construct也会得到一个右值。
16.5模板特例化
编写单一模板,使之对任何可能的模板实参都是最适合的,都能实例化,这并不总是能办到。在某些情况下,通用模板的定义对特定类型是不适合的;通用定义可能编译失败或做得不正确。
其他时候,也可以利用某些特定知识来编写更高效的代码,而不是从通用模板实例化。当不能(或不希望)使用模板版本时,可以定义类或函数模板的一个特例化版本。
// compare函数是一个很好的例子,它展示了函数模板的通用定义不适合一个特定类型(即字符指针)
// 的情况。希望compare通过调用strcmp比较两个字符指针而非比较指针值。
// 第一个版本:可以比较任意两个类型。
template<typename T> int compare(const T &, const T &);
// 第二个版本处理字符串字面常量
template<size_t N, size_t M>
int compare(const char (&)[N], const char (&)[M]);
// 但是,只有当传递给compare一个字符串字面常量或者一个数组时,编译器才会调用
// 接受两个非类型模板参数的版本。如果传递给它字符指针,就会调用第一个版本:
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // 调用第一个模板
compare("hi", "mom"); // 调用有两个非类型参数的版本
定义函数模板特例化
// compare的特殊版本,处理字符数组的指针。特例化一个函数模板时,必须为
// 原模板中的每个模板参数都提供实参,template后跟一个空尖括号对(<>)。
template<>
int compare(const char* const &p1, const char* const &p2) {
return strcmp(p1, p2);
}
函数重载与模板特例化
当定义函数模板的特例化版本时,本质上接管了编译器的工作。即,为原模板的一个特殊实例提供了定义。重要的是要弄清:一个特例化版本本质上是一个实例,而非函数名的一个重载版本。
将一个特殊的函数定义为一个特例化版本还是一个独立的非模板函数,会影响到函数匹配。
// 已经定义了两个版本的compare函数模板,一个接受数组引用参数,另一个接受
// const T&。还定义了一个特例化版本来处理字符指针,这对函数匹配没有影响。
// 下面的调用两个函数模板都是可行的,且提供同样好的匹配。但是,接受字符数组
// 参数的版本更特例化,因此编译器会选择它。
compare("hi", "mom");
// 如果将接受字符指针的compare版本定义为一个普通的非模板函数,在此情况下,
// 将会有三个可行函数,此时会选择非模板版本。
为了特例化一个模板,原模板的声明必须在作用域中。而且,在任何使用模板实例的代码之前,特例化版本的声明也必须在作用域中。原因在于,如果丢失了一个特例化版本的声明,编译器通常可以用原模板生成代码。
因此,模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是这些模板的特例化版本。
类模板特例化
// 为标准库hash模板定义一个特例化版本,可以用它来将Sales_data对象保存在无序容器中。
// 默认情况下,无序容器使用hash<key_type>来组织其元素。为了让自己的数据类型也能使用
// 这种默认组织方式,必须定义hash模板的一个特例化版本。
// 一个特例化hash类型必须定义:
// 1.一个重载的调用运算符,它接受一个容器关键字类型的对象,返回一个size_t。
// 2.两个类型成员,result_type和argument_type,对应调用运算符的返回类型和参数类型。
// 3.默认构造函数和拷贝赋值运算符(可以隐式定义)。
// 打开std命名空间,以便特例化std::hash
namespace std {
template<> // 正在定义一个特例化版本,模板参数为Sales_data。
struct hash<Sales_data> {
// 用来散列一个无序容器的类型必须定义下列类型
typedef size_t result_type;
typedef Sales_data argument_type; // 默认情况下,此类型需要==。
size_t operator()(const Sales_data &s) const;
// 类使用合成的拷贝控制成员和默认构造函数
}
// 可以在类内或类外定义特例化版本的成员
size_t hash<Sales_data>::operator()(const Sales_data &s) const {
return hash<string>()(s.bookNo) ^
hash<unsigned>()(s.units_sold) ^
hash<double>()(s.revenue);
}
} // 关闭std命名空间,注意:右花括号之后没有分号。
// 由于使用Sales_data的私有成员,必须将它声明为Sales_data的友元。
template<typename T> class std::hash; // 友元声明所需要的
class Sales_data {
friend class std::hash<Sales_data>;
// 其他成员保持一致
};
// 假定特例化版本在作用域中,当将Sales_data作为容器的关键字类型时,编译器
// 就会自动使用此特例化版本:
// 使用hash<Sales_data>和Sales_data的operator==
unordered_multiset<Sales_data> SDset;
类模板部分特例化
类模板的特例化不必为所有模板参数提供实参。可以只指定一部分而非所有模板参数,或是参数的一部分而非全部特性。一个类模板的部分特例化本身是一个模板,使用它时用户还必须为那些在特例化版本中未指定的模板参数提供实参。
// 标准库的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;
// decltype(42)为int,使用原始模板。
remove_reference<decltype(42)>::type a;
// decltype(i)为int&,使用第一个(T&)部分特例化版本。
remove_reference<decltype(i)>::type b;
// decltype(std::move(i))为int&&,使用第二个(即T&&)部分特例化版本。
remove_reference<decltype(std::move(i))>::type c;
特例化成员而不是类
可以只特例化特定成员函数而不是特例化整个模板:
template<typename T> struct Foo {
Foo(const T &t = T()) : mem(t) {}
void Bar() { /* ... */ }
T mem;
// 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()