导图社区 模板与泛型编程
c primer_模板与泛型编程都是编程范式,用于编写与特定数据类型无关的代码,以增加代码的复用性和灵活性。
编辑于2024-03-21 13:44:311
第16章 模板与泛型编程
绿色为扩展模式,它会独立地应用于包中的每个元素
匹配优先规则:普通非模板函数 > 特例化模板函数 > 通用模板函数
四、可变参数模板
定义
template <typename T, typename... Args> void foo(const T& t, const Args&... rest);
参数包
模板参数包
Args是一个模板参数包,表示有零个或多个模板类型参数
函数参数包
rest是一个函数参数包,表示零个或多个函数参数
使用
int i; double d; string s; foo(i, d, s); // 包中有2参数,实例化为void foo(const int&, const double&, const string&);
sizeof...(Args)或sizeof...(rest)可得出参数包中元素个数
包扩展
定义:将包分解为构成的元素。扩展前需要提供扩展模式,对每个元素应用模式,获得扩展后的列表,通过在模式右边放一个...号来触发扩展操作。
例1
template <typename T, typename... Args> ostream& print(ostream &os, const T&t, const Args&... rest) { return print(os, rest...); }
对Args的扩展中,编译器将模式const Args&应用到模板参数包Args中的每个元素,扩展后的结果是以逗号分开的多个类型的列表,每个类型都形如const type&.
在rest扩展中,rest是扩展模式,扩展后得到一个以逗号分隔的元素列表.
例2
template <typename... Args> ostream& errmsg(ostream& os, const Args&... rest) { return print(os, debug(rest)...); // 将rest参数包中每个元素传递给debug函数 }
errmsg(cerr, fcnName, code.num()); print(cerr, debug(fcnName), debug(code.num())); // 扩展后的样子
转发包参数
template <typename... Args> void emplace_back(Args&&... args) { alloc.construct(first++, std::forward<Args>(args)...); } emplace_back( val, str, 60);
为什么使用右值引用?在调用emplace_back时,为了保持实参的类型信息,只有当函数形参为T&&时,才能完全保持实参的属性信息。 为什么使用forward?模板中转发T&&参数时,需要保持参数的类型属性,forward可以做到。
典型应用——递归
template <typename T> ostream& print(ostream &os, const T&t) { return os << t; } // 终止递归版本,必须定义在前面,否则在递归到最后一次时编译器找不到该终止版本而报错。 template <typename T, typename... Args> ostream& print(ostream &os, const T&t, const Args&... rest) { return print(os, rest...); }
递归调用关系如下:
1.print(cout, i, s, 42); // 包中有2个参数
2.print(cout, s, 42); // 包中有1个元素
3.print(cout, 42); // 包中无元素,将调用终止版本
三、重载与模板
模板函数之间重载
template <typename T> string debug(const T& t) {} —— 版本1 template <typename T> string debug(const T* t) {} —— 版本2
string s("hi"); debug(s); // 调用版本1,因为版本2需要一个指针,而实参不是指针 debug(&s); // 两个版本都匹配,版本1中T为string*,版本2中T为string,版本2专门针对指针,更精准匹配。
模板与非模板函数重载
string debug(const string& s) {} —— 版本3,非模板函数 string s("hi"); debug(s); // 版本1和版本3均匹配,但是编译器优先选择非模板版本3
其他
对于有多个重载版本的函数,将他们的声明放在文件最前面,确保在调用这些函数时编译器能选中期望的重载版本
1.template <typename T> string debug(const T& t); 2.template <typename T> string debug(const T* t); 3.string debug(const string& s); debug(string("hi")); // 调用版本3,若版本3未先声明,编译器将选择版本1
T&&为万能引用,传递右值时将得到右值的引用,传递左值时将得到左值的引用
不管传入左值还是右值,最终返回的都是右值引用
顶层const无论在形参中还是实参中都将被忽略
二、模板实参推断
模板类型转换
模板实参推导时涉及的类型转换
1.const转换:可将一个非const的引用/指针传递给一个const的引用或指针形参
template <typename T> T fref(const T&); string s; fref (s); // s被转为const是允许的
2.数组和函数指针转换:若函数形参不是引用类型,则可将数组或函数转换为指针传递
template <typename T> T fobj(T, T); int a[10], b[42]; fobj(a, b); // 数组被转为指针,实例化为 int* fobj(int*, int*);
函数模板显示实参
某些场景下,编译器无法推断出模板实参的类型,或我们希望能控制模板实例化出想要的版本。
例1
template <typename T1, typename T2, typename T3> T1 sum(T2 a, T3 b) { return a+b; }
sum(4, 5); // 错误,调用时并未提供返回类型,故编译器无法推导返回类型 sum<int>(4, 5); // 显式指定返回类型为int
例2
template <typename T> int compare(T v1, T v2) { }
long lng; compare<int>(lng, 1024); // 我们希望实例化int版本, 故lng被转换为int
尾置返回类型
某些场景下,使用显式模板实参表示模板函数的返回类型会带来负担,使用尾置方式更优雅
例1
template <typename It> // 期望??处是decltype(*beg),但编译遇到函数参数列表前,beg都是不存在的 ?? &fn (It beg, It end) { return *beg; }
vector<int> vi = {1,2,3}; fcn(vi.begin(), vi.end()); // 如何指定返回类型?
方法1. 增加返回类型模板参数,调用时显式指定 template <typename T, typename It> T& fcn(It beg, It end) {} fcn<int>(vi.begin(), vi.end());
方法2. 使用尾置类型 auto &fn (It beg, It end) -> decltype(*beg) {}
函数指针和实参推断
例1
template <typename T> int compare(const T&, const T&); int (*pf1) (const int&, const int&) = compare; //根据指针参数类型,T被推导为int
例2
void func(int (*) (const string&, const string&)); void func(int (*) (const int&, const int&)); func(compare); // 错误,compare的T被推断为string还是int?无法确定 func(compare<int>); // 显示指定用int版本
模板实参推断和引用
从左引用函数参数 推断类型
为什么f1(ci),T被推导为const int??若T被推导为int,那么就变成了左侧黄色部分的场景,这种场景是不允许的,故模板推导中T带了const属性。
从右值引用函数参 数推断类型
template <typename T> void f3(T&& val); f3(42); // 实参是int类型的右值,模板参数T是int f3(i); // i是左值,T被推导为int&,根据折叠规则val最终类型为int&
引用折叠
X& &、X& &&、X&& &都将折叠为X&
X&& &&折叠为X&&
std::move
功能:获得一个绑定到左值上的右值引用
实现
template <typename T> typename remove_reference<T>::type&& move(T&& t) { return static_cast<typename remove_reference<T>::type&&>(t); }
原理
string s1("hello"), s2; s2 = std::move(s1);
解释:传入左值,T推导为string&,经引用折叠后参数t类型为string&,remove_reference<T>::type 为string,最终为string&& move(string& t);
string s2; s2 = std::move(string("hello"));
解释:传入右值,T被推导为string, remove_reference<T>::type为string,最终即为string&& move(string&& t);
完美转发std::forward
功能:将函数参数连同类型不变地转发给其他函数,我们需要保持参数的所有性质,包括const和左右值属性。一般用于那些定义为模板类型参数的右值引用的函数参数(T&&)
实现
template <typename T> T&& forward(typename std::remove_reference<T>::type& __t) { return static_cast<T&&>(__t); } template <typename T> T&& forward(typename std::remove_reference<T>::type&& __t) { return static_cast<T&&>(__t); }
原理
void process(const Widget& lval); void process(Widget&& rval); template <typename T> void logProcess(T&& t){ process(std::forward<T>(t)); }
Widget w; logProcess(w);
解释:传入左值,T为Widget&,forward返回一个左值引用,调用process左值版本
logProcess(std::move(w));
解释:传入右值,T为Widget,t为左值,forward后得到一个右值引用
总结: 1. 一个函数参数为右值引用(T&& t),那么它对应的实参的const属性和左值右值属性将得到保持,会全部传到t。 2. 当需要把参数 t 转发给其他函数时,使用forward会保持实参的所有细节.
一个特例化版本本质上是一个实例,而非函数名的一个重载版本
五、模板特例化
函数模板特例化
// 通用模板 template <typename T> int compare(const T&, const T&); // 下面是特例化版本 template <> int compare(const char* const &p1, const char* const &p2){}
存在多个函数模板都可匹配时,匹配优先级:
1.非模板函数
2.特例化版本
3.普通模板函数
类模板特例化
全特化
// 通用模板 template <typename T> class Blob {}; // 特例化版本 template <> class Blob<int> {};
偏特化:指定一部分而非所有模板参数,或是指定参数的一部分特性而非全部特性
函数模板不支持偏特化,必须为每个参数提供实参
// 通用版本 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; }
特例化类成员
template <typename T> struct Foo { void Bar() {} }; // 特例化成员函数Bar template <> void Foo<int>::Bar() {} // T为int时,特例化Foo函数 // 使用 Foo<int> fi; fi.Bar(); // 调用特例化的版本,Foo<int>::Bar() Foo<double> fi; fi.Bar(); // 调用普通版本,Foo<double>::Bar()
一、定义模板
函数模板
template <typename T> int compare(T v1, T v2) { }
compare(3, 5); T将被推导为int
模板参数:根据实参类型自动推导模板实参,并实例化一个特定版本的函数
类模板
template <typename T> class Blob { };
Blob<int> obj; T 被推导为int
模板参数:无法自动推导,必须显示给出模板实参
成员函数
template <typename T> void Blob<T>::check(int val, string str) {}
类外定义时,必须要有模板参数声明;类内定义不需要.
模板参数
作用域:在声明之后至声明结束之前,且会隐藏外层作用域中相同的名字
参数分类
类型参数
用typename或class指定
非类型参数
指针/左引用:必须具有静态生存期
整数:必须是常量表达式:std::array<int, 10> arr;
默认模板实参
类:template <typename T = int> class Blob {};Blob<> b;
函数:template <class T, class F = less<T>> void fun(T t, F f) {} fun(a); // 默认使用less
成员模板
不能是虚函数
普通类的 成员模板
class Debug { template <typename T> void check(T* p) { ... } //类内定义 template <typename T> void func(T* p); // 类外定义 };
template <typename T> void Debug::func(T* p) { ... } // 类外定义
Debug d; d.check(p); d.func(p); //使用时不需要显式指定类型,编译器自动推导
类模板的成员模板:类和成员各自有自己的独立模板参数
template <typename T> class Blob { template <typename It> Blob(It b, It e); // 构造函数有自己的模板参数 }; // 类外定义必须先写类的模板参数,再写成员函数的模板参数 template <typename T> template <typename It> Blob<T>::Blob(It b, It e) : ...
Blob<int> b (vec.begin(), vec.end()); // 使用时,类的类型参数显式指定,成员函数的类型参数与普通函数一样自动推导
模板编译
1
编译模板本身——只简单检查模板语法:漏分号、变量名未定义等
2
编译实例化代码——再次编译模板代码,检查类型错误等