第 16 章 模板与泛型编程
本章内容随便记记,应该后续会跟进专门的书籍仔细学习。
16.1 定义模板
16.1.1 函数模板
函数模板 function template
模板参数 template parameter
模板参数列表 template parameter list
模板定义中,模板参数列表不能为空?
使用模板时,我们隐式或显示的指定模板实参,并将其绑定到模板参数上。
实例化函数模板
实例化 instantiate
实例 instantiation
类型参数 type parameter
非类型模板参数
nontype parameter
一个非类型参数表示一个值而非一个类型。这些值必须是常量表达式,可以是整型或者指向对象或函数类型的指针或左值引用。绑定到指针或引用非类型参数的实参必须具有静态生存期。不能用一个普通 static 局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数可以用 nullptr 或值为 0 的常量表达式实例化。
inline 和 constexpr 的函数模板
template<...> inline ... f()
template<...> constexpr ... f()
模板编译
当编译器遇到一个模板定义时并不产生代码。只有当我们实例化出模板的特定版本时,编译器才会生成代码。
为了生成一个模板的实例化版本,编译器需要掌握函数模板或类模板成员函数的定义。因此函数模板和类模板的头文件通常即包含声明也包含定义。
大多数编译错误在实例化期间报告
第一阶段编译模板本身
第二阶段遇到模板检查参数数目及类型是否匹配。
第三阶段模板实例化。
16.1.2 类模板
class template
编译器不能为类模板推断模板参数类型。
模板的名字不是一个类型名。
类模板的成员函数
在类外定义模板类的函数时,需要包含模板实参,类内定义则不需要。
template<typename T>
class C {
T f();
};
template<typename T>
T C<T>::f() { ... }
在类代码内简化模板类名的使用
template<typename T> class C {
C<T> f();
};
可以简化为
template<typename T> class C {
C f();
};
模板类和友元
类与友元各自是否是模板是无关的。
template <typename> class C;
template <typename T> bool operator==(const C<T>&, const C<T>&);
template <typename T>
class C {
friend bool operator==<T>(const C<T>&, const C<T>&);
// 相同类型的友元才可以访问
};
template <typename T> class P;
class C {
friend class P<C>; // C 实例化的是 C 的友元
template <typename T> friend class P2;
// P2 的所有实例都是友元
};
template <typename T>
class C {
friend class P<T>; // P 对应 C 的类型是友元
template <typename T> friend class P2;
// P2 的所有实例都是 C 所有实例的友元
friend class P3; // P3 是非模板类,所有实例都是 C2 所有实例的友元
};
令模板自己的类型参数成为友元
template <typename T> class C {
friend T; // T 将成为 C<T> 的友元
};
模板类型别名
typedef C<T> CT;
using CT = C<T>;
template <typename T>
using PTT = pair<T, T>;
PTT<int> p;
模板类的 static 成员
同一套模板参数的类共用同一套 static 成员。
template<typename T> class C {
public:
static void f();
static int cnt; // 声明
};
template<typename T>
int C<T>::cnt = 0; // 定义
C<int> c;
C<int>::f(); // 需要指明模板参数
c.f();
16.1.3 模板参数
模板参数遵循作用域规则。但模板内不能重用模板参数名。
模板声明
声明中的参数名不必与定义中的模板参数名相同。
使用类的类型成员
默认状态下 C++ 认为作用域运算符后名字不是类型,因此要使用模板参数中的类型时需要使用 typename
关键字。
template<typename T>
typename T::value_type f() { return typename T::value_type(); }
默认模板实参
与默认函数参数类似,也可以为模板参数提供默认模板实参 default template argument
。
当一个模板定义了所有参数的默认参数,而希望使用所有默认参数时,可以使用一对 <>
。
template<typename T = int> class C {};
C<> c;
16.1.4 成员模板
普通类的成员模板
class DebugDelete {
public:
DebugDelete(std::ostream &s = std::cerr) : os(s) { }
template <typename T> void operator()(T *p) const {
os << "deleting unique_ptr" << std::endl;
delete p;
}
private:
std::ostream &os;
};
DebugDelete d;
double *p = new double;
d(p);
int* ip = new int;
DebugDelete()(ip);
unique_ptr<int, DebugDelete> up(new int, DebugDelete());
16.1.5 控制实例化
多个文件实例化相同的模板开销可能会非常大。
对此我们可以使用显式实例化 explicit instantiation
来避免开销。
extern template declaration; // 实例化声明
template declaration; // 实例化定义
当遇到 extern 声明时意味着其他地方一定有一个实例化定义。对于给定的实例化版本,可能有多个 extern 声明,和一个实例化定义。
实例化定义会实例化所有成员
与普通实例化不同,对实例化定义编译器会实例化类的所有成员,包括内联的函数成员。即使我们不使用某个成员,也会被实例化。
16.1.6 效率与灵活性
shared_ptr 能够在运行时改变删除器,因此其析构时会进行一次删除器是否存在的判断,再跳转到删除器的代码。
unique_ptr 在编译时就绑定了删除器,因此析构时会直接调用删除器。
16.2 模板实参推断
template argument deduction
16.2.1 类型转换与模板类型参数
顶层 const 无论在形参还是实参中都会被忽略。
以下转换可以在调用中应用在函数模板:
可以将一个非 const 的对象引用或指针传递给一个 const 引用或指针形参。
数组或函数指针转换。
使用相同模板参数类型的函数形参
调用时需要实参具有相同的类型,否则会报错。
16.2.2 函数模板显式实参
有时编译器无法推断模板实参类型,这时需要显式给出实参类型
template<typename T1, typename T2>
T1 f(T2 t);
long long ll;
f<int>(ll);
指定了实参类型,则可以使用正常的类型转换
template<typename T>
void f(T t1, T t2);
int i;
long long ll;
f<int>(i, ll);
16.2.3 尾置返回类型与类型转换
template<typename It>
auto fcn(It beg, It end) -> decltype(*beg) {
// ...
return *beg;
}
注意这样我们会得到一个引用类型。为了得到元素类型则需要使用标准类型转换模板
template<typename It>
auto fcn(It beg, It end) ->
typename remove_reference<decltype(*beg)>::type {
// ...
return *beg;
}
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 | usigned X | X |
否则 | T | |
make_unsigned | 带符号类型 X | unsigned X |
否则 | T | |
remove_extent | X[n] | X |
否则 | T | |
remove_all_extents | X[n1][n2]… | X |
否则 | T |
16.2.4 函数指针和实参推断
编译器使用指针类型来推断模板实参
template<typename T> int compare(const T&, const T&);
int (*pf1)(const int&, const int&) = compare;
对于下列情况需要给出模板实参
void f(int(*)(const int&, const int&));
void f(int(*)(const string&, const string&));
f(compare<int>);
16.2.5 模板实参推断和引用
从左值引用函数参数推断类型
template<typename T> void f(T&);
int i;
const int ci;
f(i); // T 为 int
f(ci); // T 为 const int
// f(5); // 错误
template<typename T> void f(const T&);
int i;
const int ci;
f(i); // T 为 int
f(ci); // T 为 int
f(5); // T 为 int
从右值引用函数参数判断类型
template<typename T> void f(T&&); // 实参
f(5); // T 为 int
引用折叠和右值引用参数
一般我们不能创建引用的引用,但有两个例外:
当将一个左值传递给函数的右值引用参数,且此参数为模板类型参数时,编译器推断模板类型参数为实参的左值引用类型。
如果我们间接的创建一个引用的引用,则这些引用形成了折叠:
X& &, X& && 和 X&& & 都折叠成类型 X&
X&& && 折叠成 X&&
弱国函数参数是一个指向模板类型参数的右值引用,则它可以被绑定到一个左值,并且如果实参是左值,则判断出的模板参数类型是左值引用,且函数参数将被实例化为一个普通左值引用参数。
这意味着可以将任意类型的实参传递给 T&&
。
编写接受右值引用参数的模板函数
由于模板参数可能推断为一个引用类型,因此代码中可能出现非常奇怪的现象。
一般会重载右值引用的函数模板
template<typename T> void f(T&&);
template<typename T> void f(const T&);
此时第一个版本绑定到可修改的右值,第二个版本绑定到左值或 const 右值。
16.2.6 理解 std::move
std::move 是如何定义的
template<typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}
std::move 是如何工作的
string s1;
s1 = std::move(string("aaa"));
对于这个 case
T 推断为 string
remove_reference<string>::type 为 string
则 move 为 string&& move(string&&)
string s1, s2;
s2 = std::move(s1);
这个 case
T 推断为 string&
remove_reference<string>::type 为 string
则 move 的实参是 string& && 折叠为 string&
则 move 为 string&& move(string&)
static_cast 将把 string& 转化成 string&&。
注意在 move 之后 s1 中的内容是不确定的。
从左值 static_cast 到一个右值引用是允许的。
16.2.7 转发
我们可以写出这样的转发代码
template<typename F, typename T>
void ff(F f, T t) {
f(t);
}
一般来说这都是可行的,但对于
void f(int &i) {
i++;
}
int i = 1;
ff(fl, i);
来说我们无法通过调用 ff 改变 i 的值。
定义能保持类型信息的函数参数
template<typename F, typename T>
void ff(F f, T&& t) {
f(t);
}
这样我们可以在调用 f 时保持 t 的 const 属性和左值/右值属性。
但当 f 的参数是右值引用时也会存在问题。
void g(int &&i);
ff(g, 10);
这里由于函数参数 t 是一个左值表达式,因此无法传递给 g 的 i。
在调用中使用 std::forward 保持类型信息。
template<typename F, typename T>
void ff(F f, T&& t) {
f(std::forward(t));
}
16.3 重载与模板
模板函数可以被另一个模板或一个普通非模板函数重载。
函数匹配规则:
对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。候选函数模板总是可行的。
对于可行函数按照类型转换来排序。
恰有一个函数比其他任何函数更优则选此函数。否则在最好的函数中
如果有一个非模板,则选此函数。
如果有一个模板比其他模板更特例化,选择这个模板。
否则调用歧义。
16.4 可变参数模板
variadic template
接受一个可变数目参数的模板函数或模板类。
可变数目参数称为参数包 parameter packet
。
模板参数包 template parameter packet
函数参数包 function parameter packet
template<typename T &t, typename... Args> // Args 模板参数包
void foo(const T &t, const Args& ... rest); // rest 函数参数包
sizeof… 运算符
template<typename T &t, typename... Args>
void foo(const T &t, const Args& ... rest) {
cout << sizeof...(Args) << endl;
cout << sizeof...(rest) << endl;
}
16.4.1 编写可变参数函数模板
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 最后一个元素时),两个模板函数都会被匹配,但第一个 print 更特例化,因此最终编译器会选择这个版本。
16.4.2 包扩展
对于参数包,除了能获取大小我们还能做的就是扩展 extend
它。当扩展一个包时我们需要提供用于每个扩展元素的模式 pattern
。
我们通过在模式右边放上 ...
来触发包扩展
template<typename T, typename... Args>
ostream& print(ostream &os, const T &t, const Args&... rest) { // 扩展 Args 生成参数列表
os << t << ", ";
return print(os, rest...); // 扩展 rest 生成实参列表
}
我们也可以写出如下的包扩展(注意 ,,,
的位置)
template<typename T> debug(T t) {}
template<typename T, typename... Args>
ostream& errorMsg(ostream &os, const Args&... rest) {
return print(os, debug(rest)...);
}
则
errorMsg(cout, 1, 2, 3, 4, 5);
// 相当于
print(cout, debug(1), debug(2), debug(3), debug(4), debug(5));
同理这里我们可以通过如下方式转发参数包
template<typename T, typename... Args>
ostream& errorMsg(ostream &os, Args&&... rest) {
return print(os, std::forward<Args>(rest)...);
}
16.5 模板特例化
template specialization
有时写出泛用于所有类型的模板并不容易,此时我们可以对于某系类型进行特化。
template<>
ostream& print(ostream &os, bool b) {
os << (b ? "True" : "False");
}
特例化的本质是实例化一个模板,而不是重载。特例化模板时原模板声明必须在作用域中。在使用模板实例的代码前,特例化声明也必须在作用域中。
类模板也可以特例化。
类模板部分特例化
我们可以只对类的一部分参数特例化,称为部分特例化 partial specialization
。函数不能部分特例化。
特例化类的成员而不是类
template<typename T> class C {
void Bar() {}
};
template<>
void C<int>::Bar() { }
Next: [Vim] usr_01