《C++ Primer》 拾遗 第 16 章 模板与泛型编程
Notes Cpp Primer
Lastmod: 2022-01-25 周二 23:09:35

第 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() { }
Prev: 《C++ Primer》 拾遗 第 14 章 重载运算与类型转换
Next: [Vim] usr_01