C++ 学习笔记之(16)-模板与泛型编程
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于OOP
能处理类型在程序运行之前都未知的情况;而在泛型编程汇总,编译时即可获知类型。
定义模板
函数模板
函数模板就是公式,可用来生成针对特定类型的函数版本。
- template \
template <typename T>
int compare(const T &v1, const T &v2)
{
if (v1 < v2) return -1;
if (v2 < v1) return 1;
return 0;
}
compare(1, 0); // T 为 int, 实例化出 int compare(const int&, const int&)
template<typename T, class U> clac(const T&, const U&);
非类型模板参数表示值,而非类型。可用特定的类型名来指定。非类型参数的模板实参必须是常量表达式
// 比较不同长度的字符串字面值 template<unsigned N, unsigned M> int compare(const char (&p1)[N], const char (&p2)[M]) { return strcmp(p1, p2); } // 实例化出int compare(const char (&p1)[3], cosnt char (&p2)[4]),编译器会在字符串字面值常量末尾插入一个空字符 compare("hi", "hello");
函数模板可声明为
inline
或constexpr
的,说明符放在模板参数列表之后,返回类型之前template<typename T> inline T min(const T&, const T&);
函数模板和类模板成员函数的定义通常放在头文件中,且模板直到实例化时才会生成代码
类模板
类模板是用来生成类的蓝图的。与函数模板不同,编译器不能为类模板推断参数类型
template <typename T> class Blob{ /* ... */ };
Bolb<int> ia = {0}; // 保存 int 的 Blob,实例化出 template <> class Blob<int> { /* ... */ };
类模板的成员函数在类外定义时,须以
template
开始,后接类模板参数列表ret-type StrBlob::member-name(param-list) // 成员函数类外定义时 template <typename T> ret-value Blob<T>::member-name(param-list) // 类模板成员函数类模板外定义时
默认情况下,对一个实例化了的类模板,其成员只有在使用时才被实例化
在类模板作用域内,可以直接使用模板名而不提供模板实参。而类模板外定义成员时,必须记住,遇到类名才表示进入类的作用域。
template <typename T> class BlobPtr{ public: BlobPtr& operator++(); // 编译器处理时相当于处理 BlobPtr<T>& operator++(); }; template <typename T> BlobPtr<T> BlobPtr::operator--(); // 返回类型在作用域外,故需要指定模板参数
类与友元各自是否是模板相互无关。若一个类模板包含一个非模板友元,则友元可以访问所有模板实例。若友元自身是模板,类可以授权所有友元模板实例,也可只授权给特定实例
新标准可为类模板定义类型别名
扫描二维码关注公众号,回复: 921673 查看本文章template<typename T> using twin = pair<T, T>; twin<string> authors; // authors 是一个 pair<string, string>
类模板可声明
static
成员, 所有实例化的类都共享相同的static
成员。要分清模板类、类以及类的对象
模板参数
模板参数会隐藏外层作用域中声明的相同名字
模板内不能重用模板参数名
typdef double A; template <typename A, typename B> void f(A a, B b) { A tmp = a; // tmp 的类型为模板参数 A 的类型,而非 double double B; // 错误:重声明模板参数 B }
模板函数声明必须包含模板参数,且名字不必与定义中相同
由于作用域运算符
::
可用来访问static
成员和类型成员。故对于模板代码来说,无法确定访问的是名字还是类型。默认情况下,C++语言假定通过作用域运算符访问的名字而不是类型。可通过使用关键字typename
显示告诉编译器改名字为一个类型// 返回 T 的 value_type 成员 template <typename T> typename T::value_type top(const T& c) { if (!c.empty()) return c.back(); else: return typename T::valye_type(); // 若`C`为空,则返回一个值初始化的元素 }
默认模板实参(default template argument):新标准可以为函数和类模板提供默认实参
// 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; }
若类模板为其所有模板参数都提供了默认实参,则若希望使用默认实参,则必须在模板名之后跟一个空尖括号
template <class T = int> class Numbers{ /* ... */ } // T 默认为 int Numbers<> ap; // 空 <> 表示希望使用默认类型 NUmbers<long double> ldp;
成员模板
一个类(无论是普通类还是类模板)可以包含本身是模板的成员函数,这种成员被称为 成员模板(member function, 成员模板不能使虚函数
class DebugDelete{
public:
DebugDelete(std::ostream &s = std::cerr): os(s) {}
template <typename T> void operator()(T *p) const
{ os << "Deleteing unique_ptr" << std::endl; delete p;}
private:
std::ostream &os;
};
double *p = new double;
DebugDelete d; // 可想 delete 表达式一样使用的对象
d(p); // 调用 DebugDelete::operator()(double *), 释放 p
类模板与其成员模板有各自独立的模板参数,但类模板外定义成员模板时,类模板的参数列表在前
template <typename T> class Blob{ template <typename It> Blob(It b, It e); // ... } // 类模板外定义 template <typename T> template <typename It> Blob<T>::Blob(It, It e) { /* ... */ }
控制实例化
当模板被使用时才会进行实例化的特性会导致问题,即多个文件中实例化相同的模板产生严重的额外开销。新标注可通过 显示实例化(explicit instantiation)解决
- 使用
extern
声明实例,且其中模板参数已被替换为模板实参,extern
表示程序其他位置已有该实例的定义 - 一个类模板的实例化会实例化该模板的所有成员,包括内联的成员函数
效率与灵活性
shared_ptr
在运行时绑定删除器, 是用户重载删除器更为方便unique_ptr
在编译时绑定删除器, 避免了间接调用删除器的u运行时开销
模板实参推断
从函数实参来确定模板实参的过程被称为 模板实参推断(template argument deduction)
类型转换与模板类型参数
将实参传递给带模板类型的函数形参时,能够自动应用的类型转换只有以下两种
const
转换:可以将一个非const
对象的引用(或指针)传递给一个const
引用(或指针)形参- 数组或函数指针转换:如果函数形参不是引用类型,则可以对数组或函数类型的实参应用正常的指针转换。一个数组实参可以转换为一个指向其首元素的指针,函数实参缓缓为指向该函数类型的指针
template <typename T> T fobj(T, T); // 实参被拷贝 template <typename T> T fref(const T&, const T&); // 引用 string s1("a value"); const string s2("another value"); fobj(s1, s2); // 调用 fobj(string, string); const 被忽略 fref(s1, s2); // 调用 fref(const string&, const string*), 将 s1 转换为 const 是允许的 int a[10], b[42]; fobj(a, b); // 调用 f(int *, int *); fref(a, b); // 错误:数组类型不匹配
如果函数参数类型不是模板参数,则对实参进行正常的类型转换
template <typename T> ostream &print(ostream &os, const T &obj) { return os << obj; } ofstream f("output"); print(f, 10); // 使用 print(ostream&, int); 将 f 转换为 ostream&
函数模板显示实参
某些情况下,编译器无法推断模板实参的类型,或希望用户控制模板实例化。当函数返回类型与参数列表中任何类型都不相同时,上述两种情况经常出现。
// 编译器无法推断T1, 它未出现在函数参数列表中
template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
// 糟糕的设计:用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3> T3 alternative_sum(T2, T1);
//T1显示指定,T2 T3 由函数参数类型推断而来
auto val3 = sum<long long>(i, lng); // long long sum(int, long),
对于模板类型参数显示指定了的函数实参,可进行正常的类型转换
template <typename T> int compare(const T &v1, const T &v2); long lng; compare<long>(lng, 1024); // 正确:实例化 compare(long, long);
尾置返回类型与类型转换
为了获得元素类型,可使用标准库的 类型转换(type transformation)模板,定义在type_traits
中
// 尾置返回允许我们在参数列表之后表明返回类型,传入序列,返回序列元素引用
template <typename It> auto fcn(It beg, It end)->decltype(*beg)
{
return *beg; // 返回序列中一个元素的引用
}
// 使用 typename 告知编译器 type 表示一个类型,传入 序列,返回序列元素类型
template <typename It>
auto fcn2(It beg, It end)->typename remove_reference<decltype (*beg)>::type
{
return *beg; // 返回序列中一个元素的拷贝
}
函数指针和实参推断
当函数参数是一个函数模板实例的地址时,程序上下文必须满足:对每个模板参数,能唯一确定其类型或值
template <typename T> int compare(const T&, const T&);
// pf1 指向实例 int compare(const int&, const int&), T 的模板实参类型为 int
int (*pf1)(const int &, const int &) = compare;
// func 的重载版本;每个版本接受一个不同的函数指针类型
void func(int (*)(const string&, const string&));
void func(int (*)(const int&, const int&));
func(compare); // 错误:使用 compare 的哪个实例?
func(compare<int>); // 正确:显示指定实例化版本 compare(const int&, const int&)
模板实参推断和引用
从函数调用中推断类型有两个规则
- 编译器会应用正常的引用板顶规则
const
是底层的,不是顶层的
从左值引用函数参数推断类型
当函数参数是模板类型参数的一个普通(左值)引用时(即,形如
T&
),只能传递给它一个左值,若实参是const
,则T
被推断为const
类型template <typename T> void f1(T&); // 实参必须是一个左值 // 对 f1 的调用使用实参所引用的类型作为模板参数类型 f1(i); // i 是一个 int, 模板参数类型 T 是 int f1(5); // 错误:传递给一个 & 参数的实参必须是一个左值
若函数参数类型为
const T&
, 可传递任何类型的实参,const
为函数参数类型的一部分,故函数实参的const
属性被忽略,且const
不是模板参数类型的一部分template <typename T> void f2(const T&); // 可以接受一个右值 // f2 中的参数是 const &, 实参中的 const 是无关的。下列调用,函数参数都被推断为 const int& f2(i); // i 是一个 int; 模板参数 T 为 int f2(ci); // ci 是一个 const int, 但模板参数 T 为 int f2(5); // 一个 const & 参数可以绑定到一个右值, T 为 int
从右值引用函数参数推断类型
当函数参数为右值引用时,可传递右值。T 的类型为右值实参类型
template <typename T> void f3(T&&);
f3(42); // 实参为 int 型右值,模板参数 T 为 int
引用折叠和右值引用参数
若传递左值给函数的右值引用参数,且右值引用指向模板类型参数(如
T&&
)时,编译器推断模板类型参数为实参的左值引用类型(T&
)引用的引用:被折叠成普通引用
X& &
、X& &&
和X&& &
都折叠成类型X&
- 类型
X&& &&
折叠成X&&
引用折叠只能应用于间接创建的引用的引用,如类型别名或模板参数
f3(i); // 实参是一个左值; 模板参数 T 是 int & f3(ci); // 实参是一个最值; 模板参数 T 是一个 const int&
右值引用常用于模板转发实参或模板重载
理解 std::move
标准库中move
函数的定义
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
string s1("hi"), s2;
s2 = std::move(string("bye")); // 正确:实例化 string&& move(string &&t)
s2 = std::move(s1); // 正确:实例化 string&& move(string &t)
- 函数参数
T&&
为指向模板类型参数的右值引用,通过引用折叠,可匹配任何类型实参 - 通过
static_cast
显示地将左值转换为右值引用
转发
某些函数需要将一个或多个实参连同类型不变地转发给其它函数,在此情况下,需要保持被转发实参的所有性质,包括实参类型是否为const
以及实参左值右值属性
- 通过将函数参数定义为指向模板类型参数的右值引用,即可保持对应实参的所有类型信息
- 通过引用参数保持
const
属性,因为引用中const
是底层的 - 若函数参数定义为
T1 &&
和T2&&
, 通过引用折叠,可保持实参的左值右值属性 - 当用于一个指向模板参数类型的右值引用函数参数(
T&&
)时,forward
会保持实参类型的所有细节
template <typename F, typename T1, typename T2>
void flip(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
重载与模板
如果有多个函数提供同样好的匹配
- 如果同样好的函数中只有一个是非模板函数,则选择此函数
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,选择此模板
- 否则,此调用有歧义
可变参数模板
可变参数模板(variadic template)即一个接受可变数目参数的模板函数或模板类,可变数目的参数被称为 参数包(parameter packet), 存在两种参数包
- 模板参数包(template parameter packet):表示零个或多个模板参数
- 函数参数包(function parameter packet):表示零个或多个函数参数
// Args 是一个模板参数包,rest 是一个函数参数包, Args/rest 表示零个或多个模板类型参数/函数参数
template <typename T, typename... Args> void foo(const T &t, const Args& ... rest)
{
cout << sizeof...(Args) << endl; // 类型参数的数目
cout << sizeof...(rest) << endl; // 函数参数的数目
}
int i = 0; double d = 3.14; string s = "how now brown cow";
// 包中有三个参数,实例化出void foo(cosnt int&, const string&, const int &, const double &);
foor(i, s, 42, d);
编写可变参数函数模板
当定义可变参数版本的print
时,非可变参数版本的声明必须在作用域中。否则,可变参数版本会无限递归
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)
{
os << t << ", ";
return print(os, rest...);
};
print(cout, i, s, 42); // 包中有两个参数,输出 0, how now brown cow, 42
包扩展
对参数包,还可扩展(expand), 扩展包时,需提供用于每个扩展元素的模式(pattern)。扩展包即将包分解为构成的元素,对每个元素应用模式,获得扩展后的列表,通过在模式右边添加省略号...
触发扩展操作
template <typename... Args> ostream &errorMsg(ostream &os, cosnt Args&... rest)
{
// print(os, debug_rep(al), debug_rep(a2), ..., debug_rep(an))
return print(os, debug_rep(rest)...);
}
errorMsg(cerr, fcnName, code.num(), otherData, "other", item); // 等价于下式
print(cerr, debug_rep(fcnName), debug_rep(code.num()), debug_rep(otherData),
debug_rep("otherData"), debug_rep(item));
// 将包传递给debug_rep; print(os, debug_rep(a1, a2, ..., an))
print(os, debug_rep(rest...)); // 错误:此调用无匹配函数
print
调用使用了模式debug_rep(rest)
, 表示对函数参数包rest
中每个元素调用debug_rep
- 扩展中的模式会独立地应用于包中的每个元素
转发参数包
在新标准下,可组合使用可变参数模板与forward
机制来编写函数,实现将实参不变地传递给其它函数
template <typename... Args> void fun(Args&&... args) // 将 Args 扩展为一个右值引用的列表
{
// work 的实参即扩展 Args 又扩展 args
work(std::forward<Args>(args)...);
}
模板特例化
通用模板的定义可能不适合特定类型,故此时可定义类或函数模板的一个特例化版本
// 第一个版本:可以比较任意两个类型
template <typename T> int compare(const T&, const T&);
// 第二个版本处理字符串字面常量,处理的是数组,不是指针,指针无法转换为数组引用
template <size_t N, size_t M> int compare(const char (&)[N], const char (&)[M]);
// compare 的特例化版本,处理字符数组的指针
template <> int compare(const char *const &p1, const char *const &p2);
const char *p1 = "hi", *p2 = "mom";
compare(p1, p2); // 调用第一个模板
compare("hi", "mom"); // 调用有两个非类型参数的版本,若存在第三个函数,即模板特例化函数,选用函数3
- 特例化的本质是实例化一个模板,而非重载它。故,特例化不影响函数匹配
- 模板及其特例化版本应该声明在同一个头文件中,所有同名模板的声明应该放在前面,然后是这些模板的特例化版本
结语
- 模板是C++语言与众不同的特性,也是标准库的基础。一个模板就是一个编译器用来生成特定类型或函数的蓝图。生成特定类或函数的过程称为实例化。我们只编写一次模板,就可以将其用于多种类型和值,编译器会为每种类型和值进行模板实例化。
- 我们既可以定义函数模板,也可以定义类模板。标准库算法都是函数模板,标准库容器都是类模板
- 显示模板实参允许我们固定一个或多个模板参数的类型或值。对于指定了显示模板实参的模板参数,可以应用正常的类型转换
- 一个模板特例化就是一个用户提供的模板实例,它将一个或多个模板参数绑定到特定类型或值上。当我们不能(或不希望)将模板定义用于某些特定类型时,特例化非常有用
- 最新C++标准的一个主要部分是可变参数模板。一个可变参数模板可以接受数目和类型可变的参数。可变参数模板允许我们编写像容器的
emplace
成员和标准库make_shared
函数这样的函数,实现将实参传递给对象的构造函数