[C++ Primer] : 第16章: 模板与泛型编程

时间:2022-05-07 21:10:59

面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况, 不同之处在于: OOP能处理类型在程序运行之前都未知的情况, 而在泛型编程中, 在编译时就能获知类型了.

函数模板

模板是C++中泛型编程的基础, 一个模板就是一个创建类或函数的蓝图或者说公式.

模板定义以关键字template开始, 后跟一个模板参数列表, 这是一个逗号分隔的一个或多个模板参数的列表, 用小于号和大于号包围起来.

template <typename T>
int compare(const T &v1, const T &v2) // 函数参数是const的引用
{
if(v1 < v2) return -1; // 仅使用 < 比较运算
if(v2 < v1) return 1;
return 0;
}

在模板定义中, 模板参数列表不能为空.

模板参数表示在类或函数定义中用到的类型或值, 当使用模板时, 我们(显式或隐式地)指定模板实参, 将其绑定到模板参数上.

实例化函数模板

编译器用函数实参来为我们推断模板实参, 编译器使用实参的类型来确定绑定到模板参数T的类型. 编译器用推断出的模板参数来为我们实例化一个特定版本的函数, 这些编译器生成的版本通常被称为模板的实例.

模板参数包括两类: 类型模板参数非类型模板参数.

模板参数类型: 一般来说可以将类型参数看做类型说明符, 就像内置类型或类类型说明符一样使用, 特别是, 类类型参数可以用来指定类型或函数的参数类型, 以及在函数体内用于变量声明或类型转换.

类型参数前必须使用关键字class或typename, 每一个参数前面都要加上.

// 错误: U之前必须加上class或typename
template <typename T, U> T calc(const T&, const U&);

在模板参数列表中, 这两个关键字含义相同, 可以互换使用, 一个模板参数列表中可以同时使用这两个关键字. (有些情况下会有所不同, 具体见Effictive C++)

非类型模板参数: 一个非类型模板参数表示一个值而非一个类型, 我们通过一个特定的类型名而非关键字class或typename来指定非类型参数. 当一个模板被实例化时, 非类型参数被一个用户提供的或编译器推断出的值所替代, 这些值必须是常量表达式, 从而允许编译器在编译时实例化模板.

template<unsigned N, unsigned M> // 直接使用特定的类型名unsigned而非class或typename
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]);

一个非类型参数可以是一个整型, 或者是一个指向对象或函数类型的指针(左值)引用, 绑定到非类型整型参数的实参必须是一个常量表达式, 绑定到指针或引用非类型参数的实参必须具有静态生存周期. 不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参, 指针参数也可以用nullptr或一个值为0的常量表达式来实例化.

在模板定义中, 模板非类型参数是一个常量值, 在需要常量表达式的地方, 可以使用非类型参数, 如制定数组的大小.

非类型模板参数的模板实参必须是常量表达式.

inline和constexpr的函数模板

函数模板可以声明为inline或constexpr的, 说明符放在模板参数列表之后, 返回类型之前.

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

编写类型无关的代码:

泛型编程的两个重要原则:

  • 模板中的函数参数是const的引用.
  • 模板函数应该尽量减少对参类型的要求

如前面的compare函数仅使用 < 来比较而没有同时使用< 和 >来比较. 这就就降低了compare函数对要处理的类型的要求, 这些类型必须支持 <, 但不必同时支持 > .

模板程序应该尽量较少对实参类型的要求.

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;
}

模板编译:

当编译器遇到一个模板定义时, 它并不生成代码. 只有当我们实例化出模板的一个特定版本时, 编译器才会生成代码.

当我们使用(而不是定义)模板时, 编译器才生成代码, 这一特性影响了我们如何组织代码以及错误何时被检测到.

当我们使用一个类类型时, 类定义必须是可用的, 但是成员函数的定义不必已经出现. 因此我们将类定义和函数声明放在头文件中, 而普通函数和类的成员函数的定义放在源文件中.

模板则不同: 为了生成一个实例化版本, 编译器需要掌握函数模板或类模板成员函数的定义. 因此, 与非模板代码不同, 模板的头文件通常既包括声明也包括定义.

函数模板和类模板成员函数的定义通常放在头文件中. 模板设计者应该提供一个头文件, 包含模板定义以及在类模板或成员定义中用到的所有名字的声明, 模板的用户必须包含模板的头文件, 以及用来实例化模板的任何类型的头文件.

大多数编译错误在实例化期间报告:

通常编译器会在三个阶段报告错误:

  • 第一个阶段是编译模板本身时. 在这个阶段编译器通常不会发现很多错误, 编译器检查语法错误.
  • 第二个阶段是编译器遇到模板使用时. 在此阶段, 编译器仍然没有很多可以检查的, 对于函数模板调用, 编译器通常会检查实参数目是否正确, 还能检查参数类型是否匹配, 对于类模板, 编译器可以检查用户是否提供了正确数目的模板实参.
  • 第三个阶段是模板实例化时, 只有这个阶段才能发现类型相关的错误, 依赖于编译器如何管理实例化, 这类错误可能在链接时报告.

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

16.1.2 类模板

类模板是用来生成类的蓝图, 与函数模板的不同之处是, 编译器不能为类模板推断模板参数类型.

实例化类模板: 当使用一个模板时, 我们必须提供额外信息, 这些额外信息是显式模板实参列表, 它们被绑定到模板参数, 编译器使用这些模板实参来实例化特定的类.

一个类模板的每个实例都形成一个独立的类. 与类模板形成的其他的类之间没有任何关联.

谨记: 类模板的名字不是一个类型名. 类模板是用来实例化类型的, 而一个实例化的类型总是包含模板参数的, 因此类模板名字加上模板参数才是一个类型名.

类模板的成员函数

可以在类模板内部或外部定义成员函数, 并且在类模板内部定义的成员函数被隐式声明为内联函数.

类模板的每个实例都有其自己版本的成员函数. 因此, 类模板的成员函数具有和模板相同的模板参数, 因而, 定义在模板之外的成员函数必须以关键字template开始, 后接类模板参数列表.

template <typename T>
ret-type Blob<T>::member-name(parm-list)

类模板成员函数的实例化

默认情况下, 一个类模板的成员函数只有当程序用到它时才进行实例化. 如果一个成员函数没有被使用, 则它不会被实例化.

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

成员函数只有在被用到时才进行实例化, 这一特性使得即使某种类型不能完全符合模板操作的要求, 我们仍然能用该类型实例化类.

在类代码内简化模板类名的使用

当我们使用一个类模板类型时必须提供模板实参, 但是在类模板自己的作用域中, 我们可以直接使用模板名而不提供实参.

template <typename T>
class BlobPtr {
public:
BlobPtr() : curr( 0 ) { };
BlobPtr& operator++(); // 直接写BlobPtr&而不是BlobePtr<T>&, 当我们处于一个类模板的作用域时, 编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样.
// 类模板内部等价写成BlobPtr<T>& operator--();
BlobPtr& operator--();
/* ....... */
private:
std::size_t curr;
};

在类模板外使用类模板名

当我们在类模板外定义成员时, 必须记住我们并不在类的作用域中, 直到遇到类名才表示进入类的作用域.

在一个类模板的作用域内, 我们可以直接使用模板名而不必指定模板实参.

template <typename T>
BlobPtr<T> BlobPtr<T>::operator++(int) // 返回类型在类的作用域之外, 我们必须指出返回类型是一个实例化的BlobePtr, 它所用的类型与类实例化所用类型一致.
{
// 在函数体内, 我们已经进入类的作用域, 因此在定义ret时无须重复模板实参
BlobPtr ret = *this; // 等价BlobPtr<T> ret = *this
++*this;
return ret;
}

类模板和友元

如果一个类模板包含一个非模板友元, 则友元被授权可以访问所有模板实例. 如果友元自身是模板, 则类可以授权给所有友元模板的实例, 也可以只授权给特定实例.

一对一友好关系

类模板与友元函数模板有相同的类型参数.

template <typename T> class BlobPtr;  // 前置声明, 在Blob中声明友元所需要的.
template <typename T> class Bolb; // 前置声明, 下面要用到.
template <typename T>
bool operator==(const Blob<T>&, const Blob<T>&);
template <typename T> class Blob
{
// 每个Blob实例将访问权限授予用相同类型实例化的BlobPtr和相等运算符
friend class BlobPtr<T>; // 此处只授权类型BlobPtr<T>为友元类, 因此需要前置声明, 让编译器知道我们将要使用的模板的存在
friend class operator==<T>(const Blob<T>&, const Blob<T>&);
};

友元声明用Blob的模板形参作为它们自己的模板实参, 因此友元关系被限定在用相同类型实例化的Blob和BlobPtr相等运算符之间.

Blob<char> ca;   // BlobPtr<char>和operator==<char>都是本对象的友元
Blob<int> ia; // BlobPtr<int>和operator==<int>都是本对象的友元

通用和特定的模板友好关系

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

为了让所有实例成为友元, 友元声明中必须使用与类模板本身不同的模板参数.

特定实例声明为友元时需要前置声明. 非特定实例即所有实例或非模板类声明为友元时无需前置声明.

// 前置声明, 在将模板的一个特定实例声明为友元时要用到的.
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; // 为了让所有实例成为友元, 友元声明中必须使用与类模板本身不同的模板参数. 如此处用的X.
// Pal3是一个非模板类, 它是C2所有实例的友元, 不需要Pal3的前置声明
friend class Pal3;
};

令模板自己的类型参数成为友元

在新标准中, 我们可以将模板类型参数声明为友元

template <typename T> class Bar {
friend T;
};

值得注意的是, 虽然友元通常来说是一个类或函数, 但是我们完全可以用一个内置类型来实例化Bar.

在C++11新标准中, 声明一个类为另外一个类的友元时, 不再需要使用class关键字, 这一点小改进带来的变化是, 我们可以为类模板声明友元了, 如

class p;
template <typename T> class people{
friend p;
}
people<p> pp; // 类型p在这里是people类型的友元
people<int> pi; // 对于int类型模板参数, 友元声明被忽略

在使用类p作为模板参数时, p是people<p>的一个友元类, 而在使用内置类型int作为模板参数的时候, people<int>会被实例化为一个普通的没有友元定义的类. 这样一来我们就可以在模板实例化时才确定一个模板类是否有友元, 以及谁是这个模板类的友元.

模板类型别名

类模板的一个实例定义了一个类类型, 与任何其他类型一样, 我们可以定义一个typedef来引用实例化的类.

typedef Blob<string> StrBlob;

由于模板不是一个类型, 我们不能定义一个typedef引用一个模板, 即无法定义一个typedef引用Blob<T>. 新标准允许我们为类模板定义一个类型别名:

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

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

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

partNo的用户需要指出pair的first成员的类型, 但不能指定second成员的类型.

类模板的static成员

template <typename T> class Foo{
public:
static std::size_t count() { return ctr; }
private:
static std::size_t ctr;
};

每个Foo的实例都有其自己的static成员实例, 即对任意给定类型X, 都有一个Foo<X>::ctr和一个Foo<X>::count成员, 所有Foo<X>类型的对象共享相同的ctr对象和count函数.

// 实例化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

定义的开始部分是模板参数列表, 随后是我们定义的成员的类型和名字, 对于从模板生成的类来说, 类名包括模板实参.

类静态成员不属于类的任何一个对象, 所以它们并不是创建类的对象的时候定义的, 这意味着它不由类的构造函数来初始化, 因此必须在类的外部定义和初始化静态成员.

与非模板类的静态成员相同, 我们可以通过类类型对象来访问一个类模板的static成员, 也可以使用作用域运算符直接访问成员, 当然为了通过类来直接访问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???

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
}

由于参数名不能重用, 所以一个模板参数名在一个特定模板参数列表中只能出现一次

template <typename V, typename V>    // 错误, 非法重用模板参数名V

模板声明

模板声明必须包含模板参数

template <typename T> int compare(const T&, const T&);
template <typename T> class Blob;
// 有几个typename代表了有几个模板参数, 后面的T, U之类的可以不写
template <typename> class BlobPtr;

与函数参数相同, 声明中的模板参数的名字不必与定义中相同.

// 3个calc都指向相同的函数模板
template <typename T> T calc(const T&, const T&); // 声明
template <typename U> U calc(const U&, const U&);
// 模板定义
template <typename Type> Type calc(const Type &a, const Type &b) { /* */ }

一个特定文件所需要的所有模板的声明通常一起放置在文件开始的位置, 出现于任何使用这些模板的代码之前.

使用类的类型成员

默认情况下, C++语言假定通过作用域运算符访问的名字不是类型(即是一个具体的对象), 因此, 如果我们希望使用一个模板参数的类型成员, 就必须显式地告诉编译器改名字是一个类型, 我们通过使用关键字typename来实现这一点.

假定T是一个类型参数的名字, 当编译器遇到如下形式的语句时:

T::size_type * p;

它需要知道我们是正在定义一个名为p的指针变量(此时T::size_type表示一个类型), 还是将一个名为size_type的static数据成员与名为p的变量相乘(T::size_type是一个类型成员还是一个数据成员).

template <typename T>
typename T::value_type top(const T &c) // typename T::value_type表示一个类型成员
{
/* .... */
}

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

默认模板实参

新标准中可以为函数和类模板提供默认模板实参, 而更早的C++标准只允许为类模板提供默认实参.

// 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; // F的类型是一个可调用对象
if (f(v1, v2)) return 1;
return 0;
}

这里我们为模板添加了第二个类型参数, 名为F, 表示可调用对象. 并定义了一个新的函数参数, 绑定到一个可调用对象上.

我们为此模板参数提供了默认实参, 并为其对应的函数参数也提供了默认实参. 默认模板实参指出使用标准库的less函数对象类, 默认函数实参指出f将是类型F的一个默认初始化的对象.

当用户调用这个版本时可以提供自己的比较操作, 但这不是必须的.

bool i = compare(0, 42);            // 使用less
Sales_data item1(cin), item2(cin);
bool j = compare(item1, item2, compareIsbn); // 使用compareIsbn, compareIsbn必须是一个可调用对象

与函数默认实参一样, 对于一个模板参数, 只有当右侧的所有参数都有默认实参时, 它才可以有默认实参.

模板默认实参与类模板

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

template <class T = int> class Numbers{
public:
Numbers(T v = 0) : val(0) { };
private:
T val;
};
Numbers<long double> lots_of_percision;
Numbers<> average_percision; // 空<>表示我们希望使用默认类型

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;
d(p); // 调用DebugDelete::operator()(double *)释放p
int * ip = new int;
DebugDelete()(ip); // 在一个临时DebugDelete对象上调用operator()(int *), DebugDelete()使用默认的构造函数创建一个临时的对象

可以用DebugDelete类型的对象来重载unique_ptr的删除器, 在尖括号内给出删除器的类型, 并提供一个这种类型的对象给unique_ptr的构造函数.

// 销毁p指向的对象, 实例化DebugDelete::operator()<int>(int *)
// 声明p的删除器类型为DebugDelete, 并在p的构造函数中提供该类型的一个未命名对象.
unique_ptr<int, DebugDelete> p(new int, DebugDelete());
// 实例化DebugDelete::operator()<string>(string *)
unique_ptr<string, DebugDelete> sp(new string, DebugDelete());
// DebugDelete的成员模板实例化样例
void DebugDelete::operator()(int *p) const { delete p; }
void DebugDelete::operator()(string *p) const { delete p; }

类模板的成员模板

对于类模板, 我们也可以为其定义成员模板. 此种情况下, 类和成员各自有自己的, 独立的模板参数.

template <typename T> class Blob{
template <typename It> Blob(It b, It e); // 类内声明
/* ...... */
}; // 类外定义成员模板
template <typename T> // 类的类型参数
template <typename It> // 构造函数的类型参数
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> a1(begin(ia), end(ia)); // 实例化Blob<int>类及其接受两个int*参数的构造函数
Blob<int> a2(vi.begin(), vi.end());// 实例化Blob<int>类及其接受两个vector<long>::iterator的构造函数
Blob<string> a3(w.begin(), w.end());// 实例化Blob<string>类及其接受两个list<const char *>::iterator的构造函数

16.1.5 控制实例化

当模板使用时才会被实例化这一特性意味着相同的实例可能出现在多个对象文件中. 在多个文件中实例化相同的模板的额外开销可能非常严重. 在新标准中可以通过显式实例化来避免这种开销.

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

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

extern template class Blob<string>;              // 声明
template int compare(const int &, const int &); // 定义

当编译器遇到extern模板声明时, 它不会在本文件中生成实例化代码. 将一个实例化声明为extern就表示承诺在程序的其它位置有该实例化的一个非extern声明(定义). 对于一个给定的实例化版本, 可能有多个extern声明, 但必须只有一个定义.

由于编译器在使用一个模板时自动对其实例化, 因此extern声明必须出现在任何使用此实例化版本的代码之前.

// Application.cc
// extern声明的这些模板类型必须在程序其他位置进行实例化
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>; // 实例化类模板的所有成员

对每个实例化声明, 在程序中某个位置必须有其显式的实例化定义.

变量重复和模板函数重复的区别:

实际上, “外部”(extern)这个概念早在C的时候已经就有了. 通常情况下, 我们在一个文件中a.c中定义了一个变量int i, 而在另外一个文件b.c中想使用它, 这个时候我们就会在没有定义变量i的b.c文件中做一个外部变量的声明. 比如:

extern int i;

这样做的好处是, 在分别编译了a.c和b.c之后, 其生成的目标文件a.o和b.o中只有i这个符号的一份定义. 具体地, a.o中的i是实在存在于a.o目标文件的数据区中的数据, 而在b.o中, 只是记录了i符号会引用其他目标文件中数据区中的名为i的数据. 这样一来, 在链接器(通常由编译器代为调用)将a.o和b.o链接成单个可执行文件(或者库文件)c的时候, c文件的数据区也只会有一个i的数据(供a.c和b.c的代码共享).

而如果b.c中我们声明int i的时候不加上extern的话, 那么i就会实实在在地既存在于a.o的数据区中, 也存在于b.o的数据区中. 那么链接器在链接a.o和b.o的时候, 就会报告错误, 因为无法决定相同的符号是否需要合并.

而对于函数模板来说, 现在我们遇到的几乎是一模一样的问题. 不同的是, 发生问题的不是变量(数据), 而是函数(代码). 这样的困境是由于模板的实例化带来的.

代码重复和数据重复不同. 数据重复, 编译器往往无法分辨是否是要共享的数据; 而代码重复, 为了节省空间, 保留其中之一就可以了(只要代码完全相同). 事实上, 大部分链接器也是这样做的. 在链接的时候, 链接器通过一些编译器辅助的手段将重复的模板函数代码fun<int>(int)删除掉, 只保留了单个副本. 这样一来, 就解决了模板实例化时产生的代码冗余问题.

对于源代码中出现的每一处模板实例化, 编译器都需要去做实例化的工作; 而在链接时, 链接器还需要移除重复的实例化代码. 很明显, 这样的工作太过冗余, 而在广泛使用模板的项目中, 由于编译器会产生大量冗余代码, 会极大地增加编译器的编译时间和链接时间. 解决这个问题的方法基本跟变量共享的思路是一样的, 就是使用“外部的”模板.

外部模板的使用实际依赖于C++98中一个已有的特性, 即显式实例化(Explicit Instantiation). 显式实例化的语法很简单, 比如对于以下模板:

template <typename T> void fun(T) {}

我们只需要声明:

template void fun<int>(int);

这就可以使编译器在本编译单元中实例化出一个fun<int>(int)版本的函数(这种做法也被称为强制实例化). 而在C++11标准中, 又加入了外部模板(Extern Template)的声明(实例化声明). 语法上, 外部模板的声明跟显式的实例化差不多, 只是多了一个关键字extern. 对于上面的例子, 我们可以通过:

extern template void fun<int>(int);

这样的语法完成一个外部模板的声明.

在使用外部模板的时候, 我们还需要注意以下问题: 如果外部模板声明出现于某个编译单元中, 那么与之对应的显示实例化必须出现于另一个编译单元中或者同一个编译单元的后续代码中; 外部模板声明不能用于一个静态(static)函数(即文件域函数), 但可以用于类静态成员函数(这一点是显而易见的, 因为静态函数没有外部链接属性, 不可能在本编译单元之外出现).

实例化定义会实例化所有成员:

当编译器遇到一个实例化定义时, 它不了解程序使用那些成员函数, 因此, 与处理类模板的普通实例化不同, 编译器会实例化该类的所有成员, 即使我们不使用某个成员, 它也会被实例化.

在一个类模板的实例化定义中, 所用类型必须能够用于模板的所有成员函数.

16.1.6 效率与灵活性

shared_ptr和unique_ptr有两方面的不同: 1.管理所保存的指针的策略, 前者共享指针, 后者独占指针. 2.允许用户重载默认删除器的方式不同. 对于shared_ptr, 只要在创建或reset指针时传递给它

一个可调用对象即可, 与之相反, 删除器的类型是一个unique_ptr对象的类型的一部分, 用户必须在定义unique_ptr时以显式模板实参的形式提供删除器的类型.

在运行时绑定删除器(shared_ptr的删除器)

可以确定shared_ptr不是将删除器直接保存为一个成员, 因为删除器的类型直到运行时才会知道. 在一个shared_ptr生存期中, 可以随时用reset改变其删除器的类型, 通常类成员的类型在运行时是不能改变的, 因此不能直接保存删除器.

shared_ptr的删除器的工作过程类似下面的形式:

// del的值只有在运行时才知道, 通过一个指针来调用它
del ? del(p) : delete p; // del(p)需要运行时跳转到del的地址

由于删除器是间接保存的, 调用del(p)需要一次运行时的跳转操作, 转到del中保存的地址来执行对应的代码.

在编译时绑定删除器(unique_ptr的删除器)

删除器类型是类类型的一部分, unique_ptr有两个模板参数, 一个表示它所管理的指针, 另一个表示删除器的类型. 由于删除器的类型是unique_ptr类型的一部分, 因此删除器成员的类型在编译时是知道的.

unique_ptr的删除器的工作过程:

// del在编译时绑定, 直接调用实例化的删除器
del(p); // 无运行时额外开销

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(s1, s2); // 调用fobj(string, string), s2的const被忽略, 在fobj调用中, 实参被拷贝, 因此源对象是否是const没有关系
fref(s1, s2); // 调用fref(const string&, const string&), 将s1转换为const是允许的.
int a[10], b[20];
fobj(a, b); // 调用fobj(int *, int *), 形参不是引用, 数组实参可以转换为指针
fref(a, b); // 错误: 数组类型不匹配, 形参是一个引用, 数组不会转换为指针. 数组大小也是数组类型的一部分, 因此a和b的类型是不匹配的, 但是函数形参确实一样的, 都是T, 因此调用错误.

将实参传递给带模板类型的函数形参时, 能够自动应用的类型转换只有const转换以及数组或函数到指针的转换.

使用相同模板参数类型的函数形参

一个模板类型的参数可以用作多个函数形参的类型, 传递给这些形参的实参必须具有相同的类型, 如果推断出的类型不匹配, 则调用错误. 如上面的fref调用.

long lng;
compare(lng, 1024); // 错误, 不能实例化compare(long, int), 算数类型不会进行类型转换

如果希望允许对函数实参进行正常的类型转换, 我们可以将函数模板定义为两个类型参数.

// 实参类型可以不同, 但必须兼容
template <typename A, typename B>
int fiexibleComlare(const A &v, const B &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) reurn 1;
return 0;
}

正常类型转换应用于普通函数实参

函数模板可以有普通类型定义的参数, 即不涉及模板类型参数的类型, 这种函数实参不进行特殊处理, 它们正常转换为对应形参的类型.

如果函数参数类型不是模板参数, 则对实参进行正常的类型转换.

template <typename T> ostream &print(ostream &os, const T &obj)
{
return os << obj;
} print(cout, 42); // 实例化pirnt(ostream&, int)
ofstream f("output");
// 第一个参数不是模板参数, 进行正常的类型转换.
print(f, 10); // 使用print(ostream&, int): 将f转换为ostream&

16.2.2 函数模板显式实参

指定显式模板实参

作为一个例子, 我们定义一个名为sum的函数模板. 编译器无法推断T1, 它未出现在函数参数列表中, 每次调用sum时调用者都必须为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);

显式模板实参按由左至右的顺序与对应的模板参数匹配: 第一个模板实参与第一个模板参数匹配, 第二个实参与第二个参数匹配, 依次类推.

只有尾部(最右)参数的显式模板实参才可以忽略, 而且前提是他们可以从函数参数推断出来.

template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1); // 糟糕的设计: 用户必须制定所有三个模板参数
auto val3 = alternative_sum<long long>(i, lng); // 错误, 不能推断前几个模板参数
auto val3 = alternative_sum<long long, int, long>(i, lng) // 正确. 显式指定了所有三个参数, 从左到右的顺序匹配, val3为long, i为int, lng为long long. 正常类型转换应用于显式指定的实参
对于模板类型参数已经显式指定了的函数实参, 也进行正常的类型转换: ```cpp
template <typename T> int compare(const T&, const T&);
long lng;
compare(lng, 1024); // 错误, 模板参数不匹配
compare<long>(lng, 1024); // 正确, 显式指定T的类型为long, 实例化compare(long, long), 1024转换为long类型
compare<int>(lng, 1024); // 正确, 显式指定T的类型为int, 实例化compare(int, int), lng转换为int类型

16.2.3 尾置返回类型与类型转换

当我们希望由用户来确定返回类型时, 用显式模板实参表示模板函数的返回类型是有效的. 但在其他情况下, 要求显式指定模板实参会给用户添加额外的负担.

// 尾置返回允许我们在参数列表之后声明返回类型
template <typename It>
auto fcn(It beg, It end) -> decltype(*beg) // 尾置返回出现在参数列表之后, 它可以使用函数的参数, 返回类型为引用类型
{
// 处理序列
return *beg; // 返回一个元素的引用
}

进行类型转换的标准库模板类

如果我们的函数想返回一个值而非引用, 函数中唯一可用的操作是迭代器操作, 而所有迭代器操作都不会生成元素, 而只生成元素的引用, 为了获得元素类型, 可以使用标准库的类型转换模板. 这些模板定义在type_traits中.

remove_reference模板有一个模板类型参数和一个名为type的public类型成员. 如果我们用一个引用类型实例化remove_reference, 则type将表示被引用的类型, 如我们实例化remove_reference<int &>, 则type成员将是int. 给定一个迭代器beg: remove_reference<decltype(*beg)>::type将获得beg引用的元素的类型, decltype返回元素类型的引用类型, remove_reference::type脱去引用, 剩下元素类型本身. 组合使用remove_reference, 尾置返回以及decltype, 我们就可以在函数中返回元素值的拷贝:

// 为了使用模板参数的成员, 必须使用typename
// 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&)
// 用函数模板compare来初始化函数指针pf1, 编译器将根据函数指针pf1的类型来推断compare的模板实参. 本例中T的类型为int
int (*pf1)(const int&, const int&) = compare;
// 如果不能从函数指针类型确定模板实参, 则会产生错误
// func的重载版本, 每个版本接受一个不同的函数指针类型
void func(int(*)(const string&, const string&));
void func(ing(*)(const int&, const int&));
func(compare); // 错误, 使用compare的那个实例?
// 问题在于通过func的参数类型无法确定模板实参的唯一类型, 对func的调用既可以实例化int版本, 也可实例化string版本. 可以通过使用显式模板实参来消除func调用的歧义:
// 正确: 显式指出实例化那个版本的compare版本
func(compare<int>); // 传递compare(const int&, const int&)

当参数是一个函数模板实例的地址时, 程序上下文必须满足: 对每个模板参数, 都能唯一确定其类型或值.

16.2.5 模板实参推断和引用

考虑下面的例子:

template <typename T> void f(T &p);

p是一个模板类型参数T的引用. 记住重要的两点: 1.编译器会应用正常的引用绑定规则, 2.const是底层的, 不是顶层的, 顶层const被忽略.

从左值引用函数参数推断类型:

当一个函数参数是模板类型参数的一个普通(左值)引用时(即形如T&), 绑定规则告诉我们只能传递给它一个左值. 如果实参是const类型, 则T将被推断为const类型.

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时, T的类型推断的结果不会是一个const类型, const已经是函数参数类型的一部分, 因此, 它不会也是模板参数的一部分:

template <typename T> void f2(const T&);   //可以接受一个右值
// f2中的参数是const &, 实参中的const是无关的
f2(i); // i是一个int, 模板参数T的类型是int
f2(ci); // ci是一个const int, 但模板参数类型T是int, 函数参数本身是const, 则模板参数的类型将会忽略const.
f2(5); // 一个const &参数可以绑定到一个右值, T是int

从右值引用函数参数推断类型:

当一个函数参数是一个右值引用时, 只能传递给它一个右值, 推断出的T的类型是该右值实参的类型:

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

引用折叠和右值引用参数

通常不能将一个右值引用绑定到一个左值上, 但是C++语言在正常绑定规则之外定义了两个例外规则, 允许这种绑定, 这两个例外规则是move这种标准库正确工作的基础.

第一个例外规则影响右值引用参数的推断如何进行. 当我们将一个左值传递给函数的右值引用参数, 且此右值引用指向模板类型参数(如T&&)时, 编译器推断模板类型参数为左值引用类型.

如上面的函数f3, 当我们调用f3(i)时, T的类型被推断为int &, 而非int, 这看起来好像意味着f3的函数参数应该是一个类型int &的右值引用. 通常我们不能直接定义一个引用的引用, 但是通过类型别名或通过模板类型参数间接定义是可以的.

第二个例外绑定规则: 如果我们间接创建一个引用的引用, 则这些引用形成了“折叠”, 在所有情况下, 引用会折叠成一个普通的左值引用类型. 新标准中, 折叠规则扩展到右值引用. 只有一种情况引用会折叠成右值引用.

  • X& &, X& &&, X&& &都折叠成类型X&.
  • X&& &&折叠成X&&.

引用折叠只能应用于间接创建的引用的引用, 如类型别名或模板参数.

f3(i)的实例化结果可能如下所示:

// 无效代码, 仅用于演示
void f3<int&>(int& &&);

f3的函数参数是T&&且T是int&, 因此T&&是int& &&, 会折叠成int&, 因此即使f3的函数参数形式是一个右值引用, 此调用也会用一个左值引用类型来实例化f3.

void f3<int &>(int &);

这两个规则导致了两个重要的结果:

  • 如果一个函数参数是一个指向模板类型参数的右值引用(如T&&), 则它可以被绑定到一个右值, 且
  • 如果一个实参是一个左值, 则推断出的模板实参类型将是一个左值引用, 且函数参数将被实例化为一个(普通)左值引用参数(T&).

注意前提是函数模板, 函数模板的参数是指向模板类型参数的右值引用. 如果是一个普通的函数, 则不适用于上述规则.

这两个规则暗示着我们可以将任意类型的实参传递给T&&类型的函数参数.

如果一个函数参数是指向模板参数类型的右值引用(如, T&&), 则可以传递给它任意类型的实参. 如果将一个左值传递给这样的参数, 则函数参数被实例化为一个普通的左值引用(T&).

编写接受右值引用参数的模板函数

template <typename T> void f3(T&& val)
{
T t = val; // 拷贝还是绑定一个引用?即T是不是一个引用类型?
t = fcn(t); // 赋值只改变t还是既改变t又改变val
if(val == t) { /*......*/ } // 如果T是引用类型, 则一直为true
}

当我们对一个右值调用f3, 如字面常量值42, T为int

当我们对一个左值i调用f3时, 遵循第一个例外规则, T为int&

在实际中, 右值引用通常用于两种情况: 模板转发其实参模板被重载

目前应该注意的是, 使用右值引用的函数模板通常使用以下方式进行重载.

template <typename T> void f(T&&);      // 绑定到非const右值
template <typename T> void f(const T&); // 左值和const右值

模板实参推断和引用总结:

  • 从左值引用推断: 模板函数参数与传递进来的实参类型一致.
  • 从右值引用来推断: 若传递进来的实参是一个左值, 则模板类型参数为实参的左值引用类型(经过了引用折叠), 即T&, 若实参是右值, 则T的类型是该右值实参的类型.

16.2.6 理解std::move

通过move可以获得一个绑定到左值上的右值引用. move本质上可以接受任何类型的实参, 因此它是一个函数模板.

std::move是如何定义的

标准库是这样定义std::move的:

template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}

首先move的函数参数是T&&是一个指向模板类型参数的右值引用, 通过引用折叠, 此参数可以与任何类型的实参匹配. 特别是既可以传递给move一个左值, 也可以传递一个右值.

string s1("hi!"), s2;
s2 = std::move(string("bye!")); // 正确, 从一个右值移动数据
s2 = std::move(s1); // 正确, 但在赋值之后, s1的值是不确定的

std::move是如何工作的

在std::move(string("bye!"))中, string("bye!")是一个右值, 向一个右值引用传递一个右值, 由实参推断出的类型为被引用的类型. 因此在std::move(string("bye!"))中

  • 推断出的T类型为string
  • remove_reference用string进行实例化
  • remove_reference的type成员是string
  • move的返回类型为string&&
  • move的函数参数t的类型为string&&

调用这个实例化move<string>, 即函数 string&& move(string &&t);

在std::move(s1)中, s1是一个左值, 向一个右值引用传递一个左值, 由引用折叠推断出的类型为左值引用, 因此在std::move(s1)中

  • 推断出的T的类型为string&
  • remove_reference用string&进行实例化
  • remove_reference<string&>的type成员是string
  • move的返回类型仍然是string&&
  • move的函数参数t实例化为string& &&, 会折叠为string&

调用这个实例化move<string &>, 即函数string&& move(string &t);

从一个左值static_cast到一个右值引用是允许的

通常static_cast只能用于其他合法的类型转换, 但是, 这里又有一条针对右值引用的特许规则: 虽然不能隐式的将一个左值转换为右值引用, 但我们可以用static_cast显式地将一个左值转换为一个右值引用.

虽然可以编写这样的代码, 但是使用标准库move函数是容易得多的方式. 而且, 统一使用std::move使得我们在程序中查找潜在的截断左值的代码变得很容易.

16.2.7 转发(forward)

某些函数需要将其一个或多个实参连同类型不变地转发给其他函数, 在此情况下, 我们需要保持被转发实参的所有性质, 包括实参类型是否是const以及实参是左值还是右值.

// 接受一个可调用对象和另外两个参数的模板, 对翻转的参数调用给定的可调用对象
// flip1是一个不完整的实现: 顶层const和引用丢失了
template <typename F, typename T1, typename T2>
void flip1(F f, T1 t1, T2 t2)
{
f(t2, t2);
}

如果可调用对象f接受一个引用参数, 则会出现问题, 如

void f(int v1, int &v2)   // v2是一个引用
{
cout << v1 << " " << ++v2 << endl;
}

当单独调用此函数时, 它会改变v2的值, 但是通过flip1调用f则不会改变v2的值:

f(42, i);         // f改变了实参i
flip1(f, j, 42); // 通过flip1调用f不会改变j

因为flip1(f, j, 42)实例化的函数模板为: void flip1(void (*pf)(int, int&), int t1, int t2); j的值首先被拷贝给t1, 然后f中的引用参数绑定到t1, 而非j, 从而不会改变j.

定义能保持类型信息的函数参数:

通过将一个函数参数定义为一个指向模板类型参数的右值引用, 我们可以保持其对应实参的所有类型信息.

如果一个函数参数是指向模板类型参数的右值引用(如 T&&), 它对应的实参的const属性和左值/右值属性将得到保持.

template <typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}

调用flip2(f, j, 42)会实例化void flip2(void (*func)(int, int&), int &t1, int &&t2); j是一个左值, 被传递给一个右值引用的模板函数参数, 估推断出T1的类型为int&, T1 &&经过引用折叠得到int &, 42是一个右值, 传递给了一个右值引用的模板函数参数, 故推断出T2的类型为int, T2 &&的类型为int &&.

flip2仍然有问题, 它不能用于接受右值引用参数的函数, 如:

void g(int&& i, int& j)
{
cout << i << " " << j <<endl;
}
flip2(g, i, 42); // 错误: 不能从一个左值实例化int&&

对于函数调用 flip2(g, i, 42)会实例化: void flip2(void (*func)(int &&, int&), int& t1, int&& t2); 传递给g的将是flip2中的函数参数t2. 函数参数与任何其他变量一样, 都是左值表达式(此处有些绕口, 我的理解是左值是有名字的, 右值是没有名字的, 有名字的函数参数是左值, 包括指向右值的引用相当于给右值取了一个别名, 则该引用是有名字的, 是一个左值).

在调用中使用std::forward保持类型信息

可以使用forward的新标准库设施来传递flip2的参数, 它能保持原始实参的类型.

forward定义在头文件utility中, 与move不同, forward必须通过显式模板实参来调用, forward返回该显式实参类型的右值引用, 即forward<T>的返回类型是T&&.

通常情况下, 使用forward传递那些定义为模板类型参数的右值引用的函数参数, 通过其返回类型上的引用折叠, forward可以保持给定实参的左值/右值属性:

template <typename Type>
void intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg)); // 必须用显式模板实参
//......
}

本例中使用Type作为forward的显式模板实参类型, 它是从arg推断出来的. arg是一个模板类型参数的右值引用, Type将表示传递给arg的实参的所有类型信息.

如果实参是一个右值, 则Type是一个普通的右值(非引用)类型, forward<Type>将返回Type&&. 如果实参是一个左值, 则Type本身是一个左值引用类型, 此时返回类型是一个指向左值引用类型的右值引用, 再次对forward<Type>的返回类型进行引用折叠, 将返回一个左值引用类型.

当用于一个指向模板参数类型的右值引用函数参数(T&&)时, forward会保持实参类型的所有细节.

使用forward再次重写翻转函数:

template <typename F, typename T1, typename T2>
void flip(F f, T1 t1, T2 t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}

与std::move相同, 对std::forward不使用using声明是一个好主意.

16.3 模板与重载

函数模板可以被另一个模板或一个普通非模板函数重载.

如果涉及函数模板, 则函数匹配规则在以下几个方面受到影响:

  • 对于一个调用, 其候选函数包括所有模板实参推断成功的函数模板实例.
  • 候选的函数模板总是可行的, 因为模板实参推断会排除任何不可行的模板.
  • 可行函数(模板与非模板)按类型转换来排序. 可以用于模板函数调用的类型转换是非常有限的, 包括const转换, 数组或函数的指针转换(形参为非引用).
  • 如果恰有一个函数提供比任何其他函数都更好的匹配, 则选择此函数, 但是如果有多个函数提供同样好的匹配, 则:
  • 如果同样好的函数中只有一个是非模板函数, 则选择此函数.
  • 如果同样好的函数中没有非模板函数, 而有多个函数模板, 且其中一个模板比其他模板更特例化, 则选择此模板.
  • 否则, 调用有歧义.

正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解.

// debug_rep函数的最通用版本, 它可以打印任何类型
template <typename T> string debug_rep(const T &t)
{
ostringstream ret;
ret << t;
return ret.str(); // 返回ret绑定的string的一个副本
}
// debug_rep更加特例化的版本, 打印指针的值, 后跟指针指向的对象
// 注意, 此函数不能用于char *, 因为IO库为char *定义了一个 << 版本, 此 << 版本假定指针表示一个空字符结尾的字符数组, 并打印数组的内容而非地址值. 如cout << p;
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();
} // 只有第一个版本的debug_rep是可行的, 第二个版本要求一个指针参数, 但此调用中传递的是一个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, 正常函数匹配规则无法区分这两个函数, 但是根据重载函数模板的特殊规则此调用被解析为debug_rep(T *), 即更特化的版本. 设计这条规则的原因是没有它, 将无法对一个const的指针调用指针版本的debug_rep, 问题在于模板debug_rep(const T&)可以用于任何类型, 包括指针类型, debug_rep(T *)则只能用于指针类型, 它比前者更加特例化. 没有这条规则, 传递const指针的调用永远是有歧义的.
const string *sp = &s;
cout << debug_rep(sp) << endl;

当有多个重载模板对一个调用提供同样好的匹配时, 应该选择最特化的版本.

// 非模板版本
string debug_rep(const string & s)
{
return ' " ' + s + ' " ';
}
cout << debug_rep(s) << endl; // 选择非模板函数

如果一个非函数模板与一个函数模板提供同样好的匹配, 则选择非模板版本.

重载模板与类型转换

对于C风格字符串指针和字符串字面常量. 考虑以下调用:

cout << debug_rep("hello world!") <<endl;

上面的三个debug_rep版本均可行:

debug_rep(const T&);      // 模板版本, T被绑定到char[10], 数组的引用
debug_rep(T *); // 模板版本, T被绑定到const char
debug_rep(const string &);// 非模板版本, 要求从const char *到string的类型转换

两个模板均提供精确匹配, 第二个模板需要进行一次数组到指针的转换, 而对于函数匹配来说, 这种转换被认为是精确匹配. 非模板版本时可行的, 但需要进行一次用户定义的类型转换, 因此他没有精确匹配那么好, 所以两个模板函数称为可能的调用函数. T*版本更加特化, 编译器选择它.

缺少声明可能导致程序行为异常

为了是char *版本的debug_rep正确工作, 在定义此版本时, debug_rep(cosnt string&)的声明必须在作用域中, 否则, 就可能调用错误的debug_rep版本.

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)
{
// 如果接收一个cosnt string&的版本的声明不在作用域中
// 返回语句将调用debug_rep(const 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; double d = 3.14; string s = "how now brown cow";
foo(i, s, 42, d); // 包中有3个参数
foo(s, 42, "hi"); // 包中有两个参数
foo("hi"); // 空包

sizeof...运算符

当我们需要知道包中有多少个元素时, 可以使用sizeof...运算符. sizeof...运算符也返回一个常量表达式, 而且不会对实参求值.

template <typename... Args> void g(Args... args)
{
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(args) << endl; // 函数参数的数目
}

16.4.1 可变参数函数模板

可变参数函数通常是递归的. 第一步调用处理包中的第一个实参, 然后用剩余实参调用自身. 为了终止递归, 我们还需要定义一个非可变参数的函数. 如:

// 用来终止递归并打印最后一个素的函数, 此函数必须在可变参数的函数模板之前声明
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的调用:

return print(os, rest...);

此调用只传递了两个实参, 其结果是rest中的第一个实参被绑定到t, 剩余实参形成下一个print调用的参数包. 因此每个调用中, 包中的第一个实参被移除, 成为绑定到t的实参.

最后一次调用时包中只有一个元素, 此时可变参数版本和非可变参数版本提供同样好的匹配, 但是非可变参数版本更加特例化, 因此编译器会选择非可变参数版本.

当定义可变参数版本的print时, 非可变参数版本的声明必须在作用域中. 否则, 可变参数版本会无限递归.

16.4.2 包扩展

扩展一个包就是将它分解为构成的元素, 对每个元素应用模式, 获得扩展后的列表. 扩展一个包时, 需要提供用于每个扩展元素的模式, 通过在模式右边放一个省略号来触发扩展操作.

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中的每个元素. 因此, 此模式的扩展结果是一个逗号分隔的零个或多个类型的列表, 每个类型都形如const type &. 如:

print(cout, i, s, 42);

此调用被实例化为:

ostream& print(ostream&, const int&, const string&, const int&);

第二个扩展发生在对print的递归调用中, 此情况下, 模式是函数参数包的名字, 此模式扩展出一个由包中元素组成的, 逗号分隔的列表. 此调用等价于

print(os, s, 42);

理解包扩展

C++语言允许更加复杂的扩展模式.

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)...); // 扩展模式为debug_rep()
}

这个print调用了模式debug_rep(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("other")),
debug_rep(item));

与之相对, 下面的调用会编译失败:

// 将包传递给debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误, 此调用无匹配函数, 扩展模式为rest...
// 等价于
print(cerr, debug_rep(fcnName, code.num(), otherData, "other", item));

在这个扩展中, 我们试图用一个五个实参的列表来调用debug_rep, 但并不存在与此调用相匹配的debug_rep版本.

扩展中的模式会独立地应用于包中的每个元素.

16.4.3 转发参数包

class StrVec{
public:
template <class... Args> void emplace_back(Args&&...);
// 其他成员的定义
};

模板参数包扩展中的模式是&&, 意味着每个函数参数将是一个指向其对应实参的右值引用.

template <class... Args>
inline void StrVec::emplace_back(Args&&... args)
{
alloc.construct(first_free++, std::forward<Args>(args)...);
}

扩展模式为: std::forward(args)...

既扩展了模板参数包, 也扩展了函数参数包, 此模式生成的元素如: std::forward(ti)

可变参数函数通常将它们的参数转发给其他函数, 这种函数通常形式如下:

建议: 转发和可变参数模板

可变参数函数通常将他们的参数转发给其他函数. 其形式如下:

// fun有零个或多个参数, 每个参数都是一个模板参数类型的右值引用
template <typename... Args>
void fun(Args&&... args) // 将Args扩展为一个右值引用的列表
{
// work的实参既扩展Args又扩展args
work(std::forward<Args>(args)...);
}

这里我们希望将fun的所有实参转发给另一个名为work的函数. 由于fun的参数是右值引用, 因此我们可以传递给它任意类型的实参; 由于我们使用std::forward传递这些实参, 因此它们的所有类型信息在调用work是都会得到保持.

16.5 模板特例化

编写单一模板, 使之对任何可能的模板实参都是最合适的, 都能实例化, 这并不总是能办到. 在某些情况下, 通用模板的定义对特定的类型是不合适: 通用定义可能编译失败或做得不正确. 当我们不能或者不希望使用模板版本时, 可以定义类或函数模板的一个特例化版本.

定义函数模板特例化

**当我们特例化一个函数模板时, 必须为原模板中的每个模板参数都提供实参. **为了指出我们正在实例化一个模板, 应该使用关键字template后跟一个空尖括号对<>.

空尖括号<>指出我们将为原模板的所有模板参数提供实参.

// compare的特殊版本, 处理字符数组的指针
// const char* const &, 一个指向const char 的const指针的引用
template <>
int compare(const char* const &p1, const char* const &p2)
{
return strcmp(p1, p2);
}

本例中我们特例化template <typename T> int compare(const T&, const T&), T为const char *.

函数重载与模板特例化

当定义函数模板的特例化版本时, 我们本质上接管了编译器的工作.** 一个特例化版本实质上是一个实例, 而非函数名的一个重载版本, 特例化不影响函数匹配.**

类模板特例化: 默认情况下, 无序容器使用hash<key_type>来组织元素, 为了让我们自己的数据类型如Sales_data也能使用这种组织方式, 必须定义hash模板的一个特例化版本.

定义该特例化版本的hash时, 唯一复杂的地方在于: 必须在原模板定义所在的命名空间中特例化它.

我们可以向命名空间添加成员, 为了达到这一目的, 首先必须打开命名空间:

// 打开std命名空间, 以便特例化std::hash
namespace std {
/* ...... */
} // 关闭std命名空间, 注意右花括号之后没有分号
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;
};
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); // 都是实例化一个临时的函数对象来调用hash函数
}
} // 使用hash<Sales_data>和operator==
unorder_multiset<Sales_data> SDset;
// hash<Sales_data>使用Sales_data的私有成员, 因此要将它声明为Sales_data的友元
template <class T> class std::hash; // 友元声明所需要的
class Sales_data {
friend class std::hash<Sales_data>;
// 其他成员的定义
};

我们的hash<Sales_data>定义以template<>开始, 指出我们正在定义一个全特例化的模板. 特例化的版本为hash<Sales_data>.

类模板部分特例化

类模板的特例化不必为所有模板参数提供实参, 可以只指定一部分模板参数, 一个类模板的部分特例化本身是一个模板, 使用它时用户还必须为那些在特例化中未指定的模板实参提供实参.

我们只能部分特例化类模板, 而不能部分特例化函数模板.

// 原始的, 最通用的版本
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 &, 使用第一个特例化版本
remove_reference<decltype(i)>::type b;
// decltype(std:move(i))>为int &&, 使用第二个特例化版本
remove_reference<decltype(std:move(i))>::type c;
// 三个变量a, b, c均为int类型

特例化成员而不是类

我们可以只特例化特定成员函数而不是特例化整个模板.

template <typename T> struct Foo{
Foo(const T &t = T()) : mem(t) { };
void Bar() {/* ... */};
T mem;
};
template <> // 我们正在特例化一个模板
void Foo<int>::Bar() // 我们正在特例化Foo<int>的成员Bar
{
/* ... */
}
Foo<string> fs; // 实例化Foo<string>::Foo()
fs.Bar(); // 实例化Foo<string>::Bar()
Foo<int> fi;
fi.Bar(); // 使用我们特例化版本的Foo<int>::Bar()