- 第 16 章 模板与泛型编程
- 16.1 定义模板
- 16.1.1 函数模板
- 16.1.2 类模板
- 16.1.3 模板参数
- 16.1.4 成员模板
- 16.1.5 控制实例化
- 16.2 模板实参推断
- 16.2.1 类型转换与模板类型参数
- 16.2.2 函数模板显示实参
- 16.2.3 尾置返回类型与类型转换
- 16.2.4 模板实参推断和引用
- 16.2.5 理解 std::move
- 16.2.6 转发
- 16.3 重载与模板
- 16.4 可变参数模板
- 16.4.1 编写可变参数函数模板
- 16.4.2 包扩展
- 16.5 模板特例化
一个模板就是一个创建类或函数的蓝图或者公式。
16.1.1 函数模板 定义一个函数模板 compare:
template
int compare(const T &v1, const T &v2) {
if (v1 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 data;
// 若 data[i]无效,则抛出msg
void check(size_type i, const std::string &msg) const;
};
(2)实例化类模板
当使用一个类模板时,我们必须提供额外信息。这些额外信息是显式模板实参列表,它们被绑定到模板参数。编译器使用这些模板实参来实例化出特定的类。
Blob ia;
Blob ia2 = { 0, 1, 2, 3, 4 };
ia 和 ia2 使用相同的特定类型版本的 Blob(即 Blob)。从这两个定义,编译器会实例化出一个与下面定义等价的类:
template class Blob {
typedef typename std::vector::size_type size_type;
Blob();
Blob(std::initializerlist il);
//...
int& operator[] (size_type i);
private:
std::shared_ptr data;
void check(size_type i, const std::string &msg)const;
};
当编译器从我们的 Blob 模板实例化出一个类时,它会重写 Blob 模板,将模板参数 T 的每个实例替换为给定的模板实参,在本例中是 int。
(3)类模板的成员函数
我们既可以在类模板内部,也可以在类模板外部为其定义成员函数,且定义在类模板内的成员函数被隐式声明为内联函数。
定义在类模板之外的成员函数就必须以关键字 template 开始,后接类模板参数列表。
(4)在类代码内简化模板类名的使用
当我们使用一个类模板类型时必须提供模板实参。
但这一规则有一个例外。在类模板自己的作用域中,我们可以直接使用模板名而不提供实参。
// 若试图访问一个不存在的元素,BlobPtr 抛出一个异常
template class BlobPtr {
public:
BlobPtr() : curr(0) { }
BlobPtr(Blob &a, size_t sz = 0) : wptr(a.data), curr(sz) { }
T& operator* () const {
auto p = check(curr, "dereference past end");
return(*p)[curr]; // (*p)为本对象指向的 vector
}
// 递增和递减
BlobPtr& operator++(); //前置运算符
BlobPtr& operator--();
private:
// 若检查成功,check 返回一个指向 vector 的 shared_ptr
std::shared_ptr check(std::size t, const std::string&) const;
// 保存一个 weak_ptr,表示底层 vector 可能被销毁
std::weak_ptr wptr;
std::size_t curr; // 数组中的当前位置
};
BlobPtr 的前置递增和递减成员返回 BlobPtr&,而不是BlobPtr&。当我们处于一个类模板的作用域中时,编译器处理模板自身引用时就好像我们已经提供了与模板参数匹配的实参一样。
(5)在类模板外使用类模板名
当我们在类模板外定义其成员时,我们并不在类的作用域中,直到遇到类名才表示进入类的作用域:
// 后置:递增 / 递减对象但返回原值
template
BlobPtr BlobPtr::operator++(int) {
// 此处无须检查;调用前置递增时会进行检查
BlobPtr ret = *this; // 保存当前值
++*this; // 推进一个元素;前置++检查递增是否合法
return ret; // 返回保存的状态
}
在函数体内,我们已经进入类的作用域,因此在定义 ret 时无须重复模板实参。因此,ret 的定义与如下代码等价;
BlobPtr ret = *this;
(6)类模板和友元
如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。
-
一对一友好关系
// 前置声明,在Blob中声明友元所需要的 template class BlobPtr; template class Blob; //运算符 == 中的参数所需要的 template bool operator==(const Blob&, const Blob&); template class Blob { // 每个 Blob 实例将访问权限授予用相同类型实例化的 BlobPtr 和相等运算符 friend class BlobPtr; friend bool operator==(const Blob&, const Blob&); };
友元的声明用 Blob 的模板形参作为它们自己的模板实参。因此,友好关系被限定在用相同类型实例化的 Blob 与 BlobPtr 相等运算符之间。
-
通用和特定的模板友好关系
一个类也可以将另一个模板的每个实例都声明为自已的友元,或者限定特定的实例为友元:
// 前置声明,在将模板的一个特定实例声明为友元时要用到 template class Pal; class C { // C 是一个普通的非模板类 friend class Pal; // 用类 C 实例化的 Pal 是 C 的一个友元 // Pal2 的所有实例都是 C 的友元;这种情况无须前置声明 template friend class Pal2; }; template class C2 { // C2 本身是一个类模板 // C2 的每个实例将相同实例化的 Pal 声明为友元 friend class Pal; // Pal 的模板声明必须在作用域之内 // Pal2 的所有实例都是 C2 的每个实例的友元,不需要前置声明 template friend class Pal2; // Pal3 是一个非模板类,它是 C2 所有实例的友元 friend class Pal3; //不需要 Pal3 的前置声明 };
为了让所有实例成为友元,友元声明中必须使用与类模板本身不同的模板参数。
-
令模板自己的类型参数成为友元
在新标准中,我们可以将模板类型参数声明为友元:
template class Bar { friend Type; // 将访问权限授予用来实例化 Bar 的类型 //... };
此处我们将用来实例化 Bar 的类型声明为友元。因此,对于某个类型名 Foo,Foo 将成为 Bar 的友元,Sales_data 将成为 Bar 的友元,依此类推。
(7)模板类型别名
typedef Blob StrBlob;
// C++11 新标准方式
template using twin = pair;
twin area; // area 是一个 pair
(8)类模板的 static 成员
template class Foo {
public:
static std::size_t count() { return ctr; }
// 其他接口成员
private:
static std::size_t ctr;
// 其他实现成员
}
template
size_t Foo::ctr = 0; // 初始化 ctr
类似任何其他成员函数,一个 static 成员函数只有在使用时才会实例化。
16.1.3 模板参数(1)模板声明
与函数参数相同,声明中的模板参数的名字不必与定义中相同:
// 3 个 calc 都指向相同的函数模板
template T calc(const T&,const T&); // 声明
template U calc(const U&,const U6); // 声明
// 模板的定义
template
Type calc(const Type& a, const Type& b) { /* ...*/ }
(2)使用类的类型成员
假定 T 是一个类型参数的名字,当编译器遇到如下形式的语句时:
T::size_type *pi
它需要知道我们是正在定义一个名为 p 的变量还是将一个名为 size_type 的 static 数据成员与名为 p 的变量相乘。
默认情况下,C++ 语言假定通过作用域运算符访问的名字不是类型。因此,如果我们希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。我们通过使用关键字 typename 来实现这一点:
template
typename T::value_type top(const T& c) {
if(!c.empty())
return c.back();
else
return typename T::value_type();
}
(3)默认模板实参
我们也可以提供默认模板实参。在新标准中,我们可以为函数和类模板提供默认实参。而更早的 C++标准只允许为类模板提供默认实参。
例如,我们重写 compare,默认使用标准库的 less 函数对象模板:
// compare 有一个默认模板实参 less 和一个默认函数实参 F()
template
int compare(const T &vl, const T &v2, F f = F()) {
if (f(vl, v2)) return -1;
if (f(v2, v1)) return 1;
return 0;
}
(4)模板默认实参与类模板
无论何时使用一个类模板,我们都必须在模板名之后接上尖括号。尖括号指出类必以须从一个模板实例化而来。
特别是,如果一个类模板为其所有模板参数都提供了默认实参,且我们希望使用这些默认实参,就必须在模板名之后跟一个空尖括号对:
template class Numbers { // T默认为int
public:
Numbers(T v = 0): val(V) { }
// 对数值的各种操作
private:
T val;
};
Numbers lots_of_precision;
Numbers average_precision; // 空表示我们希望使用默认类型
16.1.4 成员模板
成员模板不能是虚函数。
(1)普通(非模板)类的成员模板
与任何函数模板相同,T 的类型由编译器推断。
(2)类模板的成员模板
与类模板的普通函数成员不同,成员模板是函数模板。当我们在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。类模板的参数列表在前,后跟成员自己的模板参数列表:
template // 类的类型参数
template // 构造函数的类型参数
Blob::Blob(It b, It e) : data(std::make_shared(b, e)) { }
(3)实例化与成员模板
与普通函数模板相同,编译器通常根据传递给成员模板的函数实参来推断它的模板实参。
16.1.5 控制实例化 当两个或多个独立编译的源文件使用了相同的模板,并提供了相同的模板参数时,每个文件中就都会有该模板的一个实例。
在大系统中,在多个文件中实例化相同模板的额外开销可能非常严重。在新标准中,我们可以通过显式实例化来避免这种开销。一个显式实例化如下:
extern template class Blob; // 实例化声明
template class Blob; // 实例化定义
当编译器遇到 extern 模板声明时,它不会在本文件中生成实例化代码。将一个实例化声明为 extern 就表示承诺在程序其他位置有该实例化的一个非 extern 声明(定义)。对于一个给定的实例化版本,可能有多个 extern 声明,但必须只有一个定义。
由于编译器在使用一个模板时自动对其实例化,因此 extern 声明必须出现在任何使用此实例化版本的代码之前。
一个类模板的实例化定义会实例化该模板的所有成员,包括内联的成员函数。
16.2 模板实参推断 16.2.1 类型转换与模板类型参数 可以应用于模板函数的类型转换有以下两种:
-
const 转换
可以将一个非 const 对象的引用(指针)传递给一个 const 的引用(指针)形参
-
数组或函数指针转换
如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换:
- 一个数组实参可以转换为指向其首元素的指针
- 一个函数实参可以转换为一个该函数类型的指针
其他类型转换(如,算术转换、派生类向基类的转换等)都不能应用于函数模板。
(1)使用相同模板参数类型的函数形参
compare 函数接受两个 const T& 参数,其实参必须是相同的类型:
long lng;
compare(lng, 1024); // 错误,不能实例化 compare(long, int)
如果希望对函数实参进行正常的类型转换,可以将函数模板定义为两个类型参数:
template
int flexibleCompare(const A& v1, const B& v2) {
if (v1
typename remove_reference::type {
// 处理
return *beg; // 返回引用
}
16.2.4 模板实参推断和引用
(1)从左值引用函数参数推断类型
-
T&
template void f1(T&); // 实参必须是一个左值 f1(i); // i 是一个 int,模板参数类型 T 是 int f1(ci); // ci 是一个 const int,模板参数类型 T 是 const int f1(5); // 错误
-
const T&
template void f2(const T&); // 可以接受一个右值 f2(i); // i 是一个 int,模板参数类型 T 是 int f2(ci); // ci 是一个 const int,模板参数类型 T 是 int f2(5); // 一个 const & 参数可以绑定到一个右值,T 是 int
(2)从右值引用函数参数推断类型
template void f3(T&&);
f3(42); // 实参是一个 int 类型的右值,模板参数 T 是 int
(3)引用折叠和右值引用参数
我们通常不能将一个右值引用绑定到一个左值上,例如 f3(i)
这样的调用,我们认为是不合法的。但是 C++ 允许两种例外:
- 当将一个左值(i)传递给函数的模板类型右值引用参数(T&&)时,编译器推断模板类型参数(T)为实参的左值引用类型
- 如果间接创建了一个引用的引用(如第一个规则),则这些应用会进行折叠:
- X& &、X& &&、X&& & 都折叠成 X&
- X&& && 折叠成 X&&
正是因为这两个例外,导致了以下两个重要结果:
- 如果一个函数参数是一个指向模板类型参数的右值引用(如,T&&),则它可以被 绑定到一个左值;且
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将 被实例化为一个(普通)左值引用参数(T&)
template void f3(T&&);
f(i); // T 推断为 int&
f(ci); // T 推断为 const int&
(4)编写接受右值引用参数的模板函数
以上接受右值引用的模板函数代码对于实参类型的不同,可能会有出乎意料的结果:
template
void f3(T&& val) {
T t = val; // T 是引用类型还是一个值类型?t 是一个引用还是一个拷贝?
t = fcn(t); // 是同时改变 val 还是只改变 t?
if (val == t) { /* ... */ } // 如果 t 是引用,则判断结果一直为 true
}
因此实际中,右值引用的函数模板通常使用以下方式进行重载:
template void f(T&&); // 绑定到非 const 右值
template void f(const T&); // 左值和 const 右值
16.2.5 理解 std::move
标准库的 move 定义:
template
typename remove_reference::type&& move(T&& t) {
return static_cast (t);
}
move 利用了上一节介绍的 C++ 的两个特例,其中
- 参数
T&& t
使得 move 可以接受所有类型的实参 remove_reference
使得无论 T 是引用还是值类型,都返回对应的值类型- 返回类型
remove_reference::type&&
使得move 总是返回一个右值引用
某些函数需要将其一个或多个实参连同类型不变地转发给其他函数。在此情况下,我们需要保持被转发实参的所有性质,包括实参类型是否是 const 的以及实参是左值还是右值。
例如,我们编写一个函数,接受一个可调用表达式和两个额外的实参,并将这两个实参逆序传递给这个表达式:
// 接受一个可调用对象和另外两个参数的模板
// 对"翻转"的参数调用给定的可调用对象
// flip1 是一个不完整的实现:顶层 const 和引用丢失了
template
void flip1(F f, T1 tl, T2 t2) {
f(t2, t1);
}
当我们传递的实参是引用时,可以发现,f(t2, t1)
传递的可能是值,因此并没有达到原始期待的效果。
(1)使用右值形参优化
按照之前介绍的内容,我们可以使 T 变为 T&&:
template
void flip2(F f, T1&& tl, T2&& t2) {
f(t2, t1);
}
这样优化的代码可以很好地保持翻转实参的 const、左值和右值属性。
但是这样的版本只能解决一半的问题,却不能处理接受右值引用参数的函数。
假设我们传进去的表达式 g 定义如下:
void g(int &&i, int &j) {
cout
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?