第十六章 模板和泛型编程
面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。
- OOP能处理类型在程序运行之前都未知的情况;
- 泛型编程中,在编译时就可以获知类型。
定义模板
- 模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。
函数模板
template <typename T> int compare(const T &v1, const T &v2){}
- 模板定义以关键字
template
开始,后接模板形参表,模板形参表是用尖括号<>
括住的一个或多个模板形参的列表,用逗号分隔,不能为空。 - 使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。
- 模板类型参数:类型参数前必须使用关键字
class
或者typename
,这两个关键字含义相同,可以互换使用。旧的程序只能使用class
。 - 非类型模板参数:表示一个值而非一个类型。实参必须是常量表达式。
template <class T, size_t N> void array_init(T (&parm)[N]){}
- 内联函数模板:
template <typename T> inline T min(const T&, const T&);
- 模板程序应该尽量减少对实参类型的要求。
- 函数模板和类模板成员函数的定义通常放在头文件中。
类模板
- 类模板用于生成类的蓝图。
- 不同于函数模板,编译器不能推断模板参数类型。
定义类模板:
template <class Type> class Queue {};
- 实例化类模板:提供显式模板实参列表,来实例化出特定的类。
- 一个类模板中所有的实例都形成一个独立的类。
- 模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
类模板的成员函数:
template <typename T> ret-type Blob::member-name(parm-list)
- 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。
- 新标准允许模板将自己的类型参数成为友元。
template <typename T> class Bar{friend T;};
。 - 模板类型别名:因为模板不是一个类型,因此无法定义一个
typedef
引用一个模板,但是新标准允许我们为类模板定义一个类型别名:template<typename T> using twin = pair<T, T>;
模板参数
- 模板参数与作用域:一个模板参数名的可用范围是在声明之后,至模板声明或定义结束前。
- 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。
- 当我们希望通知编译器一个名字表示类型时,必须使用关键字
typename
,而不能使用class
。 - 默认模板实参:
template <class T = int> class Numbers{}
成员模板
成员模板(member template):本身是模板的函数成员。
- 普通(非模板)类的成员模板。
- 类模板的成员模板。
控制实例化
- 动机:在多个文件中实例化相同模板的额外开销可能非常严重。
显式实例化:
extern template declaration; // 实例化声明
template declaration; // 实例化定义
效率与灵活性
模板实参推断
- 对函数模板,编译器利用调用中的函数实参来确定其模板参数,这个过程叫模板实参推断。
类型转换与模板类型参数
能够自动转换类型的只有:
- 和其他函数一样,顶层
const
会被忽略。 - 数组实参或函数实参转换为指针。
- 和其他函数一样,顶层
函数模板显式实参
- 某些情况下,编译器无法推断出模板实参的类型。
- 定义:
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
- 使用函数显式实参调用:
auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来
- 注意:正常类型转换可以应用于显式指定的实参。
尾置返回类型与类型转换
- 使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
template <typename It> auto fcn(It beg, It end) -> decltype(*beg)
- 尾置返回允许我们在参数列表之后声明返回类型。
标准库的类型转换模板:
- 定义在头文件
type_traits
中。 | 对Mod<T>
,其中Mod
是: | 若T
是: | 则Mod<T>::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
|
函数指针和实参推断
- 当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。
模板实参推断和引用
- 从左值引用函数推断类型:若形如
T&
,则只能传递给它一个左值。但如果是const T&
,则可以接受一个右值。 - 从右值引用函数推断类型:若形如
T&&
,则只能传递给它一个右值。 引用折叠和右值引用参数:
- 规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
T&&
),编译器会推断模板类型参数为实参的左值引用类型。 规则2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型
X
:X& &
、X& &&
和X&& &
都折叠成类型X&
。- 类型
X&& &&
折叠成X&&
。
上面两个例外规则导致两个重要结果:
- 1.如果一个函数参数是一个指向模板类型参数的右值引用(如
T&&
),则它可以被绑定到一个左值上; - 2.如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(
T&
)。
- 1.如果一个函数参数是一个指向模板类型参数的右值引用(如
- 规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如
理解std::move
- 标准库
move
函数是使用右值引用的模板的一个很好的例子。 - 从一个左值
static_cast
到一个右值引用是允许的。
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
转发
- 使用一个名为
forward
的新标准库设施来传递参数,它能够保持原始实参的类型。 - 定义在头文件
utility
中。 - 必须通过显式模板实参来调用。
forward
返回显式实参类型的右值引用。即,forward<T>
的返回类型是T&&
。
重载与模板
- 多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
- 非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
可变参数模板
可变参数模板就是一个接受可变数目参数的模板函数或模板类。
可变数目的参数被称为参数包。
- 模板参数包:标识另个或多个模板参数。
- 函数参数包:标识另个或者多个函数参数。
- 用一个省略号来指出一个模板参数或函数参数,表示一个包。
template <typename T, typename... Args>
,Args
第一个模板参数包。void foo(const T &t, const Args& ... rest);
,rest
是一个函数参数包。sizeof...
运算符,返回参数的数目。
编写可变参数函数模板
- 可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
包扩展
- 对于一个参数包,除了获取它的大小,唯一能做的事情就是扩展(expand)。
- 扩展一个包时,还要提供用于每个扩展元素的模式(pattern)。
转发参数包
- 新标准下可以组合使用可变参数模板和
forward
机制,实现将实参不变地传递给其他函数。
模板特例化(Specializations)
- 定义函数模板特例化:关键字
template
后面跟一个空尖括号对(<>
)。 - 特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
- 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
- 我们可以部分特例化类模板,但不能部分特例化函数模板。