第十六章 模板和泛型编程
16.1 定义模板
- 模板参数列表:
<>
- 模板参数(也称(模板)类型参数):
T
typename
或class
(作用相同),用来表示模板参数
- 模板非类型参数:模板参数列表中表示一个值而非一个类型
函数模板
- 模板实例化时,可以使用[[ch16-模板和泛型编程#函数模板显式实参|显式实参]],根据实参[[ch16-模板和泛型编程#类型转换与模板类型参数|隐式推断模板参数类型]]
- 模板非类型参数:
- 可以是一个整型、指针或左值引用
- 实例化时,整型实参必须是常量表达式,指针/引用指向的对象必须有静态的生存周期(即对象不能是非static局部变量或动态对象),这样做可以使编译器在编译时实例化模板
- 使用场景:比如数组类型作为模板参数时大小固定,但是使用模板非类型参数就不必固定
1 2 3 4
template <typename T, unsigned N, unsigned M> inline T compare(const char (&r1)[N], const char (&r2)[M]) { return strcmp(r1, r2); }
inline
或constexpr
的函数模板:模板参数列表之后,返回类型之前- 模板编译
- 只有模板实例化时,编译器才生成代码
- 通常将类定义和函数声明放在头文件中,其实现放在源文件中;但是,函数模板和类模板成员函数的实现通常也放在头文件中。因为编译器知道模板的完整定义后才能进行实例化
- 大多数模板的编译错误在实例化期间才报告
类模板
- 使用类模板必须提供显式模板参数列表,编译器不能推断模板参数类型
- 一个类模板的成员函数只有当程序用到它时,才进行实例化
- 使用类模板类型时必须提供模板参数,只有在类模板作用域内部才可以只使用模板名而不提供实参
- 友元相关,例子
- 如果一个类模板包含一个非模板友元,则友元可以访问该类模板的所有实例
- 如果一个类模板包含一个模板友元,则类可以授权给所有友元模板实例,也可以只授权给特定实例
- 一对一友好关系:友好关系被限定在相同类型的友元和类本身之间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
template<typename> class Fri; template<typename> class MyClass; template<typename T> bool operator== (const MyClass<T>&, const MyClass<T>&); template<typename T> class MyClass{ friend class Fri<T>; // 比如Fri<int>实例可以访问MyClass<int>实例 friend bool operator==(const MyClass<T>, const MyClass<T>); // 友元函数,重载== public: MyClass() {}; MyClass(const MyClass& m): v(m.v) {} void push_back(T val) { v.push_back(val); } MyClass clone(); // 处于类模板作用域中,编译器会将MyClass当作是MyClass<T> private: vector<T> v; }; template<typename T> MyClass<T> MyClass<T>::clone() { return MyClass(*this); } template<typename T> class Fri{ public: // 一对一友好关系,Fri<T>是MyClass<T>的友元 void size(MyClass<T> m) { cout<<m.v.size()<<endl; } // 但是Fri<T>就不是MyClass<K>的友元(T!=K) void func(MyClass<int> m) { cout<<m.v.size()<<endl; } }; int main(){ MyClass<char> char_class; MyClass<int> int_class; Fri<char> fri; fri.size(char_class); // fri.func(int_class); }
- 通用和特定的模板友好关系:一个类可以将另一个模板的每个实例都声明为为自己的友元,或者限定特定的实例为友元
1 2 3 4 5 6 7 8 9 10 11
template<typename T> class Fri; class Common{ // 普通类 template<typename K> friend class Pal; // 【Pal的所有实例】都是类Common的友元,此时Fri可以不用提前声明 }; template<typename T> class MyClass{ // 模板类 friend class Fri<T>; // 一对一友元 template<typename K> friend class MyFri; // 【MyPal的所有实例】都是MyClass每个实例的友元,此时MyPal可以不用提前声明,且声明中使用了不同的模板参数 friend class Commom_fri; // 普通类作为模板类的友元,此时不需要提前声明 };
- 一对一友好关系:友好关系被限定在相同类型的友元和类本身之间
- 虽然友元通常是类或函数,但是允许将模板类型参数
T
作为友元,因此类型T
的对象可以访问类模板的private成员。例子。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#include <iostream> using namespace std; template<typename T> class MyClass{ friend T; // 类型T是MyClass的友元 private: void private_func() { cout<<"private func of MyClass "<<endl; } }; class Test{ public: void func(){ test.private_func(); } private: MyClass<Test> test; // 类型Test是MyClass的友元,Test类型的对象可以访问【类模板MyClass基于Test类型的实例】的private成员 }; int main(){ Test t; t.func(); }
- 模板类型别名
- 为类模板的实例创建别名:
typedef MyClass<int> MC;
- 为类模板定义别名:
1 2 3
template<typename K, typename V> using p = std::pair<K, V>; template<typename K> using p = std::pair<K, std::string>; // 固定一个类型 template<typename T> using p = std::pair<T, T>;
- 为类模板的实例创建别名:
- 类模板的static成员
- 相同类型类模板的实例的static成员是共享的,不同类型之间类模板的实例的static成员是不同的
模板参数
- 模板内不能重用模板参数名,且同一个模板参数名
T
在同一个模板参数列表中只能出现一次 - 声明中的模板参数不必与定义中的模板参数相同,且模板声明通常一起放在文件开始位置(使用模板的代码之前)
- 使用
T::mem
,无法判断mem是类型T的static成员还是类型T的类型成员- 普通类中编译器已知类的定义因此可以判断
- 默认
T::mem
访问的是static成员 - 使用
typename
显式说明访问的是类型1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
class Person{ public: using age = std::size_t; // 类型成员 static std::string name; // static成员 }; std::string Person::name = "zhang"; template<typename T> class MyClass{ public: typename T::age func(){ // 显式说明T::age是一个类型而非static成员 std::cout<<T::name<<std::endl; // 默认T::name是static成员 typename T::age myAge = 10; return myAge; } };
- 默认模板实参:默认模板实参都在最右侧,对函数模板和类模板都可以提供默认模板实参
成员模板
- 成员模板:普通类或模板类的成员函数是模板函数,成员模板不能是虚函数
- 普通类的成员模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <iostream> using namespace std; class MyClass{ // 普通类中包含成员模板 public: template<typename T> void func(T); }; // 普通类中的成员模板 template<typename T> void MyClass::func(T t){ cout<<t<<endl; } template<typename T> class Test{ public: void func(T); }; // 对比模板类中的普通成员函数 template<typename T> void Test<T>::func(T t){ cout<<t<<endl; } int main(){ MyClass myClass; myClass.func<int>(1); // 普通类中的成员模板 Test<int> test; test.func(2); // 模板类中的普通函数 }
- 类模板的成员模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
#include <iostream> #include <string> using namespace std; template<typename T> class MyClass{ public: MyClass() {} template<typename K> MyClass(K k) {cout<<typeid(T).name()<< " "<< typeid(K).name()<<endl;} template<typename H> void func(H); }; template<typename T> // 类模板的模板参数列表 template<typename H> // 类模板的成员模板的模板参数列表 void MyClass<T>::func(H h){ cout<<typeid(T).name()<< " "<< typeid(H).name()<<endl; } int main(){ MyClass<int> myClass('k'); // 显式提供T=int, 隐式推断K=char myClass.func(3.14); // 隐式推断H=double myClass.func<string>("str"); // 显式提供H=string }
显式实例化
- 背景:模板只有使用时才被实例化,因此相同的实例可能出现在多个文件中,造成额外开销
- 显式实例化
- 当编译器遇到
extern
模板声明时,不会在本文件中生成模板的实例化代码,表示使用其他位置的实例化代码 extern
声明必须出现在使用此实例化版本的代码之前,否则编译器进行实例化,起不到外部定义的效果- 显式实例化定义会实例化所有成员
- 当编译器遇到
|
|
效率与灵活性
- shared_ptr在运行时绑定删除器,因此删除器保存为一个指针而不是一个成员,因此删除器的类型直到运行时才直到,而且可以随时改变删除器的类型
- 需要间接调用删除器,但是用户重载删除器的操作更加便捷(只需要传入一个可调用对象)
- unique_ptr在编译期绑定删除器,删除器的类型是类类型的一部分(因此删除器类型在编译器是已知的),从而删除器可以直接保存在成员中
- 避免了间接调用删除器的运行时开销
16.2 函数模板实参推断
- 可以[[ch16-模板和泛型编程#类型转换与模板类型参数|基于实参推导模板参数类型]],也可以[[ch16-模板和泛型编程#函数指针和实参推断|函数指针指向函数模板推导模板参数类型]]
- 有时比如返回值类型无法推导出来,可以指定[[ch16-模板和泛型编程#函数模板显式实参|模板显式实参]],也可以使用[[ch16-模板和泛型编程#尾置返回类型与类型转换|尾置返回类型]](同时可以[[ch16-模板和泛型编程#尾置返回类型后进行类型转换|去除引用]])
- 当模板类型
T
为引用时,需要依据实参是左值/右值来判断T的引用类型,而且可以使用完美转发保持实参的类型不变
类型转换与模板类型参数
- 模板实参推断:根据实参类型推断出模板参数
T
的类型- 一般
T
就是实参类型 - 编译器会对以下几种实参进行类型转换,得到的
T
并不完全是实参类型- 顶层
const
会被忽略 - 可以将非const对象的指针或引用传递给一个const的指针或引用形参
- 如果形参不是引用类型,数组名/函数名转换为指针类型
- 顶层
- 一般
template<typename T> void func(T a, T b);
中,a和b推断的类型必须相同
函数模板显式实参
- 背景:类型
T
只出现在返回值/函数体,不在形参列表中时,编译器无法推断出模板实参的类型: - 函数模板显式实参从左到右进行对应,如果模板参数可以推导出来,放在模板参数列表右侧,实例化时可以进行推导
- 当显式指定实参时,对实参可以使用正常的类型转换
|
|
尾置返回类型与类型转换
尾置返回类型
- 背景:有时函数模板的返回值类型不能由实参推导而来,比如[[ch16-模板和泛型编程#函数模板显式实参|函数模板显式实参]]的背景
- 可以使用函数模板显式实参进行指定,也可以使用尾置返回类型自动推导
|
|
尾置返回类型后进行类型转换
- 背景:通过模板显式实参可以指定返回值类型,但是通过尾置返回类型推导得到的类型可能不是想要的,比如有时不希望得到引用类型
- 标准库的类型转换模板
- 定义在头文件
type_traits
中,常用于模板元程序设计 - Mod是一个类模板,将类型
T
转换为类型type
;如果无法转化,则类型type
- 定义在头文件
|
|
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 |
函数指针和实参推断
- 将函数模板赋值给函数指针:
- 函数指针指向函数模板的一个实例
- 使用函数指针的类型来推断模板实参
1 2
template <typename T> int compare(const T&, const T&); int (*f)(const int&, const int&) = compare; // T是int,f指向函数模板的实例compare(const int&, const int&)
- 如果函数func的形参是函数指针,实参可以传入函数模板,这样函数指针指向一个模板实例且进行了参数推断
- 但是当func有多个重载的版本时(接受不同类型的函数指针),传入函数模板可能产生歧义,此时可以指定【显式模板实参】
1 2 3 4 5
template <typename T> int compare(const T&, const T&); void func(int(*fp)(const int&, const int&)); void func(int(*fp)(const std::string&, const std::string&)); // 重载 // func(compare); // 歧义:函数模板compare实例化为哪一种函数指针? func(compare<int>);
模板实参推断和引用
模板参数T&
的类型推断
模板类型参数 | 实参要求 | 例子 |
---|---|---|
T& | 必须传递一个左值 | 实参为int ,T 为int ;实参为const int ,T 为const int |
const T& | 可以传递左值或右值 | 实参为int, const int, const int&& ,T 都为int |
T&& | 必须传递一个右值 | 实参为int&& , T 为int |
T&& | 例外:传递一个类型为type 的左值,推导T 为type& 类型 | 实参为int& ,T 为int& |
引用折叠
- 背景:基于上面这个例外,创造出了引用的引用,或者通过类型别名也可以创造出引用的引用,可以将多个引用折叠为一个引用
- 引用折叠规则:
type& &、 type& &&、 type&& &
折叠为type &
type&& &&
折叠为T&&
- 使用:
- 若函数形参是
T&&
,则可以传递一个左值(实参的const属性可以保持,因为是底层const) - 如果传入的是左值
type&
,推导出T=type&
- 万能引用(或称为模板类型参数右值引用):函数形参为
T&&
时,传递左值/右值均可- 使用万能引用导致只有在运行时才能确定形参是左值还是右值,使得模板的编写变得困难
- 通常在两种情况中使用万能引用:模板转发实参,模板重载
- 使用万能引用的形参通常重载为两个版本:
- 拷贝版本:
template<typename T> void f(const T&)
绑定到左值和const右值 - 移动版本:
template<typename T> void f(T&&)
绑定到非const右值
- 拷贝版本:
- 若函数形参是
std::move
- [[ch13-拷贝控制#^d3a2a0|std::move函数]]的定义:
1 2 3 4
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
- 可以使用
static_cast
显式地将左值转换为右值引用
转发
- 背景:比如函数f内部调用函数g时,可能需要将f的实参传递给g,而且同时要求保持实参性质不变(比如const属性,左值/右值属性)
- 即希望达到这样的效果:将实参传递给f,再传递给g,与实参直接传递给g,的效果等价
- 例子一:f形参类型是非引用,g形参类型是左值引用,传入一个左值引用,则此时f转发参数给g时会使用自己的拷贝而非原来的引用
- 例子二:f形参类型是万能引用
T&&
,g形参类型是右值引用,f可以接受右值(或左值),此时f转发参数给g时,使用的右值引用本身是一个左值,不能传参给g的右值引用
- 比较:都是定义在
utility
中的函数模板,最好显式指明是std::
中的std::forward
:可指定模板参数,并且可以对返回值使用引用折叠来保留左右值属性std::move
:返回值一定是右值引用
- 使用:完美转发
- 通过万能引用在传入外层f时保留实参的全部属性
- 通过
std::forward
在传入内层函数g时再次保留实参的全部属性
1 2 3 4
template <typename T> void f(T &&arg){ // 万能引用 g(std::forward<T>(arg)); // std::forward<T>的返回类型是T&& };
- 如果实参是左值,推导出
T=type&
,std::forward<T>
的返回类型T&&
折叠为type&
- 如果实参是右值,推导出
T=type
,std::forward<T>
的返回类型即为type&&
- 例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
#include <iostream> #include <utility> using namespace std; template<typename F, typename T, typename K> void flip(F f, T &&t, K &&k){ f( std::forward<K>(k), std::forward<T>(t) ); } template<typename K, typename T> void g(K&& k, T&& t) { cout<<typeid(K).name()<<" "<<typeid(T).name()<<" "<<k<<" "<<t<<endl; } void f(int &&k, int &t) { cout<<k<<" "<<t<<endl; } int main(){ int t = 2; flip(f, t, 3); // F=void(*)(int&&, int&), T=int&, K=int flip(g<int, int&>, t, 3); // F=void(*)(int&&, int&), T=int&, K=int }
16.3 重载与模板
- 函数模板可以被另一个函数模板或普通函数重载
- [[ch06-函数#6.6 函数匹配|函数的重载与匹配]]:有一些重载的函数,根据实参情况,调用时用哪个函数?
- 函数模板的重载与匹配:有一些重载的函数模板和普通函数,根据实参情况(函数模板进行函数模板实参推断),实例化并调用哪个函数模板
- 到函数模板的函数匹配规则([[ch06-函数#6.6 函数匹配|普通函数匹配的拓展]])
- 确定候选函数:同名的函数,包括实参推断成功的函数模板实例
- 确定可行函数:参数类型和数量都匹配,
- 候选的函数模板实例都是可行的,因为实参推断会排除掉不可行的模板
- 按照类型转换进行排序,寻找最佳匹配(普通函数和函数模板实例都可能发生类型转换,只是应用于函数模板的[[ch16-模板和泛型编程#类型转换与模板类型参数|类型转换]]十分有限)
- 若恰有一个函数提供比其他函数都好的匹配,则选择它
- 如果多个函数都提供相同级别的匹配
- 非模板和模板重载:如果只有一个是非模板函数,选择非模板函数
- 多个可行模板:如果没有非模板函数(有多个函数模板),选择最特例化的函数模板(特例化:比如
T&
可以匹配任意类型,T*
只能匹配指针类型) - 调用有歧义,失败
- 最佳实践:在定义重载函数之前,应该先声明所有重载的版本,否则可能重载一个模板函数进行实例化(因为没有找到想使用的版本,使用函数模板进行实例化),编译期不会报错,但是运行期会调用不期望使用的版本
16.4 可变参数模板
- 可变参数模板就是一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为参数包
- 模板参数包:表示零个或多个模板参数(模板类型参数或模板非类型参数)
typename
后跟...
表示模板类型参数包- 类型名后跟
...
表示模板非类型参数包
- 函数参数包:表示零个或多个函数参数。
- 模板参数包:表示零个或多个模板参数(模板类型参数或模板非类型参数)
sizeof...
运算符:返回参数包中的元素数量,且不会对其实参求值(类似于sizeof)1 2 3 4
template<typename T, typename... Args> // 模板参数包:Args是模板类型参数包 void func(const T &t, const Args& ... rest){ // 函数参数包:rest是函数参数包(其类型是模板参数包Args) cout<<sizeof...(Args)<<" "<<sizeof...(rest)<<endl; }
编写可变参数函数模板
- [[ch06-函数#可变形参|可变形参]]
initializer_list
可以接受可变数目实参,但是需要是相同类型的 - 可变参数函数通常是递归的
- 第一步调用处理参数包中的第一个实参,然后用剩余实参调用自身
- 还需要定义一个非可变参数的函数(因为函数匹配时会使用这个更加特例化的版本,而不是使用0个参数的可变参数模板的实例),来处理参数包中最后一个实参
|
|
包扩展
- 扩展
...
:将参数包分解为单个元素,每个元素应用模式,得到拓展后的列表 - 常用情况:
const Args& ...
:将模板参数包Args
中所有类型T
都扩展为const T&
args...
:将函数参数包args
扩展为参数列表f(args)...
:对函数参数包args
中每个元素调用函数f
1 2 3 4
template<typename... Args> ostream& msg(ostream& os, const Args&... rest){ // 扩展模板参数包 return print(os, debug_reg(rest)...); // 扩展函数参数包 }
转发参数包
- 组合使用可变参数模板和
forward
机制,实现将可变参数的完美转发,例子:emplace_back
- 如果同时存在模板参数包和函数参数包,则同时拓展:
f<Args>(args)...
等价于f<Args1>(args1), f<Args2>(args2), ...
|
|
16.5 模板特例化(Specializations)
- 背景和例子:对于某个类型,不想用(对特定类型可以做优化)或者不能用(对特定类型的使用并非预期)模板
定义函数模板特例化
- 必须为原模板中每个模板参数都提供实参(全特例化)
- 关键字
template
后面跟一个空尖括号对(<>
),表示所有模板参数都已被指定 - 特例化版本的参数类型必须与一个先前声明的模板中对应的类型相匹配。
1 2 3 4 5
template <typename T> bool compare(const T&, const T&) {} template <> bool compare(const char* const &, const char* const &); // T为const char*
函数重载与模板特例化
- 特例化的本质是实例化一个模板,而不是重载它。因此特例化不影响[[ch16-模板和泛型编程#16.3 重载与模板|函数的匹配规则]],即非模板函数先,再是特例化版本的函数(模板的实例化),最后是函数模板的实例
- 最佳实践:模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
1 2 3 4 5 6 7
template <typename T> void compare(const T& l, const T& r); template <> void compare(const char* const &l, const char* const &r); // 特例化版本,T为const char* void compare(const char* const &l, const char* const &r); // 普通函数
类模板特例化
- 必须在原模板定义所在的命名空间中进行类模板特例化
- 类模板可以进行部分特例化(偏特例化),得到的是模板;也可以全部特例化(全特例化),得到的是实例
- 类模板的部分特例化
- 未完全确定类型的模板参数仍放在
<>
中,即偏特化的模板参数列表非空。使用时也需提供模板实参,这些实参与原始模板中的参数按位置对应 - 部分特例化的模板参数列表是原始模板参数列表的一个子集或者特例化版本
- 例子:标准库remove_reference类型是通过一系列的特例化版本来完成其功能的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
//原始的、最通用的版本,可用于任意类型实例化 template<typename T> struct remove_reference { typedef T type; }; //部分特例化版本 template<typename T> struct remove_reference<T&> { //针对于左值引用的 typedef T type; }; template<typename T> struct remove_reference<T&&> { //针对于右值引用的 typedef T type; }; int i; //调用原始模板 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; //a、b、c均为int
- 未完全确定类型的模板参数仍放在
- 特例化成员函数而不是整个类
- 使用模板的实例调用成员时,若该实例的模板实参与特化该成员时的参数一致,则调用特化版本的成员
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
template<typename T> struct Foo { Foo(const T &t = T()) :mem(t) {} void Bar() {std::cout<<1<<std::endl;} //通用的Bar()函数 T mem; }; //特例化Foo<int>版本的的成员Bar template<> void Foo<int>::Bar() {std::cout<<2<<std::endl;} int main(){ Foo<std::string> fs; fs.Bar(); //使用Foo<string>的通用的Bar() Foo<int> fi; fi.Bar(); //使用Foo<int>的特例化的Bar() }