第十六章 模板与泛型编程
- 面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。
- OOP能处理类型在程序允许之前都未知的情况。
- 泛型编程在编译时就可以获知类型。
一、定义模板
-
模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者公式。
1. 函数模板
- 一个函模板就是一个公式,可用来生成特定类型的函数版本。大致格式为:
template <typename T>
int compare(const T &v1, const T &v2) {}
- 在模板定义中,模板参数列表不能为空。
- 模板参数列表的作用很像函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时,调用者提供实参来初始化形参。
- 模板参数表示在类或者函数定义中用到的类型或值。当使用模板时,我们显式或者隐式地指定模板实参,将其绑定到模板实参上。
-
模板类型参数:类型参数前必须使用关键字
class
和typename
,这两个关键字含义相同,可以互相使用。旧的程序只能class
。
-
非类型模板参数:表示一个值而非一个类型。实参必须是常量表达式。
template <typename T, size_t N>
void array_init(T (&parm)[N]) {}
- 函数模板可以声明为
inline
或constexpr
的。如同非模板函数一样。inline
或constexpr
说明符放在模板参数列表之后,返回类型之前。
template <typename T>
inline T min(const T&, const T&) {}
- 模板程序应该尽量减少对实参类型的要求。
-
函数模板和类模板成员函数的定义通常放在头文件中。
- 模板直到实例化时才会生成代码,这一特性影响了何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报告错误。
-
第一个阶段:在编译模板本身时,通常只能是检查语法错误。
-
第二个阶段:在编译器遇到模板使用时,对于函数模板调用,编译器通常会检查实参数目是否正确,以及参数类型是否匹配等,对于类模板调用,编译器可以检查是否提供了正确数目的模板实参。
-
第三个阶段:在模板实例化时,可以发现类型相关的错误。依赖于编译器如何管理实例化,这些错误可能在链接时才能报告。
2. 类模板
- 类模板是用来生成类的蓝图的。不同于函数模板,编译器不能为类模板推断模板参数类型。
- 为了使用类模板,必须在模板名后的尖括号中提供额外信息 —— 用来代替模板参数的模板实参列表。
template <typename T> class Blod {
public:
typedef T value_type;
typedef typename std::vector<T>::size_type size_type;
// 构造函数
Bold();
Bold(std::initializer_list<T> il);
// Bold中的元素数目
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
// 添加和删除元素
void push_back(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;
};
- 实例化类模板:提供显式模板实参列表,来实例化出特定的类。一个类模板的每个实例都形成一个独立的类。
-
模板形参作用域:模板参数的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
- 类模板的成员函数:
template <typename T> ret-type Bold::member-name(parm-list);
- 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
- 在一个类模板的作用域内,我们可以直接使用模板名而不必指定模板实参。
- 新标准允许模板将自己的类型参数成为友元。
template <typename T> class Bar{ friend T;}
- 模板类型别名:因为模板不是一个类型,因此无法定义一个
typedef
引用一个模板,但是新标准允许我们为类模板定义一个类型别名:
template <typename T> using twin = pair<T, T>;
3. 模板参数
-
模板参数与作用域:遵循普通的作用域规则,一个模板参数名的可用范围是在其声明之后,至模板声明或定义结束之前。
- 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置,出现于任何使用这些模板的代码之前。
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字
typename
,而不能使用class
。
- 默认模板实参:
// 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;
}
4. 成员模板
- 成员模板:一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数。成员模板不能是虚函数。
5. 控制实例化
- 由于在多个文件中实例化相同模板的额外开销可能非常严重,采取显式实例化可以避免这种开销。
-
显式实例化:
extern template declaration; //实例化声明
template declaration; // 实例化定义
- 对每个实例化声明,在程序某个位置必须有其显式的实例化定义。
- 由于实例化定义会实例化所有成员,所以在一个类模板的实例化自定义中,所用类型必须能用于模板的所有成员函数。
6. 效率和灵活性
二、模板实参推断
- 对于函数模板,编译器利用调用中的函数实参来确定其模板参数。从函数实参来确定模板实参的过程被称为模板实参推导。
1. 类型转换与模板类型参数
- 顶层const无论式在形参中还是实参中,都会被忽略。
-
const转换:可以将一个非
const
对象的引用(或指针)传递给一个const的引用(或指针)形参。
-
数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针。类似的,一个函数实参可以转换为一个该函数类型的指针。
- 其他类型转换,例如算术转换、派生类向基类的转换以及用户定义的转换等,都不能应用于函数模板。
-
注意:将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有
const
转换及数组或函数到指针的转换。
- 如果函数参数类型不是模板参数,则对实参进行正常的类型转换。
2. 函数模板显式实参
- 在某些情况下,编译器无法推断出模板实参的类型。
-
定义:
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
auto val3 = sum<long long>(i, lng); // 使用函数显式实参调用,T1是显式指定,T2和T3都是从函数实参类型推断而来
3. 尾置返回类型与类型转换
-
使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
template <typename It>
auto fun(It beg, It end) -> decltype(*beg)
- 尾置返回允许我们在参数列表之后声明返回类型。
- 标准库的类型转换模板,定义在头文件
type_traits
中:
对Mod,其中Mod为 |
若T为 |
则Mod::type为 |
remove_reference |
X&或X&& |
X |
|
否则 |
T |
add_const |
X&或const X或函数 |
T |
|
否则 |
const T |
add_lvalue_reference |
X& |
T |
|
X&& |
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 |
带符号类型 |
unsigned X |
|
否则 |
T |
remove_extent |
X[n] |
X |
|
否则 |
T |
remove_all_extents |
X[n1][n2]... |
X |
|
否则 |
T |
4. 函数指针和实参推断
- 当参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值。
5. 模板实参推断和引用
-
从左值引用函数参数推断类型:若形如
T&
,则只能传递给它一个左值。但如果const T&
,则可以接受一个右值。
-
从右值引用函数推断类型:若形如
T&&
,则只能传递给它一个右值。
-
引用折叠和右值引用参数:
-
规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
T&&
),编译器会推断模板类型参数为实参的左值引用类型。
-
规则2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型
X
:
-
X&&
、X& &&
和X&& &
都折叠成类型X&
- 类型
X&& &&
折叠成X&&
- 上面两个例外规则导致两个重要结果:
- 如果一个函数参数就是一个指向模板类型参数的右值引用(如
T&&
),则它可以被绑定到一个左值上。
- 如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(
T&
)。
6. 理解std::move
- 从一个左值
static_cast
到一个右值引用是允许的。
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
7. 转发
- 使用一个名为
forward
的新标准设施来传递参数,它能够保持原始实参的类型。
- 定义在头文件
utility
中。
- 必须通过显式模板实参来调用。
-
forward
返回显式实参类型的右值引用。即forward的返回类型是T&&
。
三、重载与模板
- 多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
- 非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
四、可变参数模板
-
可变参数模板就是一个接受可变数目参数的模板函数或模板类。
- 可变数目的参数被称为参数包
-
模板参数包:标识零个或多个模板参数
-
函数参数包:表示零个或多个函数参数
- 用一个省略号来指出一个模板参数或函数参数表示一个包。
// Args是一个模板参数包;rest是一个函数参数包;
// Args表示零个或多个模板类型参数
// rest表示零个或多个函数参数
template <typename T, typename... Args>
void foo(const T &t, const Args& ... rest);
template<typename ... Args> void g(Args ... args) {
cout << sizeof...(Args) << endl; // 类型参数的数目
coutt << sizeof...(args) << endl; // 函数参数的数目
}
1. 编写可变参数函数模板
- 当定义可变参数版本的print时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归。
2. 包扩展
- 对于一个参数包,除了获取它的大小,唯一能做的事情就是拓展。
- 当拓展一个包时,还要提供用于每个拓展元素的模式。
- 扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。
template <typename T, typename... Args>
ostream & print(ostream &os, const T &t, const Args&... rest) { // 扩展Args
os << t << ", ";
return print(os, rest...); // 扩展rest
}
3. 转发参数包
- 在C++11中,可以组合使用可变参数模板与
forward
机制来编写函数,实现将其实参不变地传递给其他函数。
五、模板特例化
- 定义函数模板特例化:关键字
template
后面跟一个空尖括号对(<>
)。
- 特例化地本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
- 模板将其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,后面是特例化版本。
- 我们可以部分特例化类模板,但不能部分特例化函数模板。