面向对象编程和泛型编程都能处理在编写程序时不知道类型的情况。

  • OOP能处理类型在程序运行之前都未知的情况
  • 泛型编程中,在编译时就需要获知类型

定义模板

  • 模板:模板是泛型编程的基础。一个模板就是一个创建类或函数的蓝图或者说公式。

函数模板

  • template <typename T> int compare(const T &v1, const T &v2) {}

  • 模板定义以关键字 template开始,后接模板形参表,模板形参表是用尖括号<>括住的一个或多个模板形参的列表,用逗号分隔,不能为空

  • 使用模板时,我们显式或隐式地指定模板实参,将其绑定到模板参数上。

  • 模板类型参数:类型参数前必须使用关键字class或者typename,这两个关键字含义相同,可以互换使用。旧的程序只能使用class
    注意每个参数前都要加classtypename,比如这样是错误的:template <typename T, U> T calc(const T&, const U&); U之前必须加上typename

  • 非类型模板参数:表示一个值而非一个类型。其模板实参必须是常量表达式template <class T, size_t N> void array_init(T (&parm)[N]){}

  • 绑定到非类型整型的实参必须是一个常量表达式。
    绑定到指针或引用非类型参数的实参必须具有静态的生存期,不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。

  • 内联函数模板中inlineconstexpr说明符放在模板参数列表之后,返回类型之前template <typename T> inline T min(const T&, const T&);

  • 模板程序应该尽量减少对实参类型的要求,比如:

    • 函数参数是**const的引用**,这样就能用于不能拷贝的类型;
    • 函数体中的条件判断仅使用<比较运算或less,这样类型就不必同时支持>
  • 函数模板和类模板成员函数的定义通常放在头文件中,即模板的头文件通常既包括声明也包括定义。

关键概念:模板和头文件

  • 当使用模板时,模板的提供者必须保证所有不依赖于模板参数的名字必须是可见的,即需要保证模板的定义包括类模板的成员的定义必须是可见的。
  • 模板的用户需要保证,用来实例化模板的所有函数、类型以及与类型关联的运算符的声明都必须是可见的。

模板的设计者应该提供一个头文件,包含模板定义以及在类模板或成员定义中用到的所有名字的声明;模板的用户必须包含模板的头文件。

大多数编译错误在实例化期间报告

  1. 编译模板本身时,编译器只能检查语法错误,比如忘记分号或变量名拼写;
  2. 模板使用时,仍然没有很多可检查的。
    对于函数模板调用,编译器通常会检查实参数目和类型是否匹配;而对于类模板,编译器只会检查模板实参的数目。
  3. 模板实例化时,只有这个阶段才会发现类型相关的错误。依赖于编译器如何管理实例化,这类错误可能在链接时才报告
    比如当编译器处理compare函数模板是,不能验证实参是否真的定义了<运算符,这类错误直到实例化时才会被发现。

练习16.1

Q:给出实例化的定义。

A:当调用一个函数模板时,编译器会利用给定的函数实参来推断模板实参,用此实际实参代替模板参数来创建出模板的一个新的“实例”, 也就是一个真正可以调用的函数,这个过程称为实例化

练习16.4

Q:编写行为类似标准库 find 算法的模版。函数需要两个模版类型参数,一个表示函数的迭代器参数,另一个表示值的类型。使用你的函数在一个 vector<int> 和一个list<string>中查找给定值。

A:

1
2
3
4
5
template<typename Iterator, typename Value>
Iterator find(Iterator first, Iterator last, const Value& v) {
for ( ; first != last && *first != value; ++first);
return first;
}

练习16.5

Q:为6.2.4节中的print函数编写模版版本,它接受一个数组的引用,能处理任意大小、任意元素类型的数组。

A:

1
2
3
4
5
template<typename Array>
void print(const Array& arr) {
for (const auto& elem : arr)
std::cout << elem << std::endl;
}

练习16.6

Q:你认为接受一个数组实参的标准库函数 beginend 是如何工作的?定义你自己版本的 beginend

A:

1
2
3
4
5
6
7
8
9
template<typename T, unsigned N>
T* begin(const T (&arr)[N]) {
return arr;
}
.
template<typename T, unsigned N>
T* end(const T (&arr)[N]) {
return arr + N;
}

类模板

  • 类模板用于生成类的蓝图。

  • 不同于函数模板,编译器不能推断模板参数类型

  • 定义类模板

    • template <class Type> class Queue {};
  • 实例化类模板:提供显式模板实参列表,来实例化出特定的类。

  • 一个类模板中所有的实例都形成一个独立的类。

  • 模板形参作用域:模板形参的名字可以在声明为模板形参之后直到模板声明或定义的末尾处使用。
    当使用一个类模板类型时必须提供模板参数,但是有一个例外:在类模板自己的作用域中,可以只使用模板名而不提供实参。

  • 类模板的成员函数:

    • template <typename T> ret-type Blob<T>::member-name(parm-list)
  • 默认情况下,对于一个实例化了的类模板,其成员只有在使用时才被实例化。

  • 类与友元各自是否是模板是无关的,可以建立一对一友好关系,也可以建立通用和特定的模板友好关系。

  • 新标准允许模板将自己的类型参数成为友元。template <typename T> class Bar{friend T;};

  • 模板类型别名:因为模板不是一个类型,因此无法定义一个typedef引用一个模板,但是新标准允许我们为类模板定义一个类型别名:template<typename T> using twin = pair<T, T>;使用:twin<string> parents;

  • 类模版的static成员:

    1
    2
    3
    4
    5
    6
    template <typename T> class Foo {
    public:
    static std::size_t cout() { return ctr; }
    private:
    static std::size_t ctr;
    }

    对任意给定类型X,都有一个Foo<X>::ctr和一个Foo<X>::cout成员。
    所有Foo<X>类型的对象共享相同的ctr对象和count函数。

  • 类似任何其他成员函数,一个static成员函数只有在使用时才会实例化

模板参数

  • 与其他任何名字一样,模板参数会隐藏外层作用域中生声明的相同名字。
    但是,与大多数其他上下文不同,在模板内不能重用模板参数名。

  • 一个特定文件所需要的所有模板的声明通常一起放置在文件开始位置。

  • 对于T::mem这样的代码

    • 在非模板代码中,编译器掌握类的定义,它通过作用域运算符知道访问的名字是类型还是static成员。
    • 但在模板代码中,编译器不知道mem是一个类型成员还是一个static数据成员。
      • 默认情况下,假定访问的是名字,而非类型。
      • 因此,如果希望使用一个模板类型参数的类型成员,就必须显示告诉编译器该名字是一个类型,需要通过关键字typename来说明这一点return typename T::value_type();
  • 当我们希望通知编译器一个名字表示类型时,必须使用关键字typename,而不能使用class

  • 默认模板实参:template <class T = int> class Numbers{}

    1
    2
    3
    4
    5
    6
    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;
    }

TODO: F f = F() 用法看不懂

练习16.17

Q: 声明为 typename 的类型参数和声明为 class 的类型参数有什么不同(如果有的话)?什么时候必须使用typename

A: 没有什么不同。默认情况下,C++假定通过作用域运算符访问的名字是static数据成员,当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename,而不能使用 class

练习16.18

Q:解释下面每个函数模版声明并指出它们是否非法。更正你发现的每个错误。

1
2
3
4
5
(a) template <typename T, U, typename V> void f1(T, U, V);
(c) inline template <typename T> T foo(T, unsigned int *);
(d) template <typename T> f4(T, T);
(e) typedef char Ctype;
template <typename Ctype> Ctype f5(Ctype a);

A:

  • (a) 非法。应该为 template <typename T, typename U, typename V> void f1(T, U, V);
  • © 非法。应该为 template <typename T> inline T foo(T, unsigned int*);
  • (d) 非法。应该为 template <typename T> T f4(T, T);
  • (e) 合法。但Ctype 被隐藏了。

练习 16.19

Q:编写函数,接受一个容器的引用,打印容器中的元素。使用容器的 size_typesize 成员来控制打印元素的循环。

A:注意:默认情况下,C++假定通过作用域运算符访问的名字是static数据成员,当我们希望通知编译器一个名字表示类型时,必须使用关键字 typename

1
2
3
4
5
template<typename Container>
void print(const Container& c) {
for (typename Container::size_type i = 0; i != c.size(); ++i)
std::cout << c[i] << " ";
}

成员模板

  • 成员模板(member template):本身是模板的函数成员,成员模板不能是虚函数。
    • 普通(非模板)类的成员模板。eg. unique_ptr<int, DebugDelete> p(new int, DebugDelete() 其中 DebugDelete() 是一个临时对象作为删除器,调用DebugDelete 对象会delete其给定的指针:DebugDelete()(ip);

    • 类模板的成员模板,必须同时为类模板和成员模板提供模板参数列表。

      1
      2
      3
      4
      5
      6
      7
      8
      template <typename T>
      template <typename It>
      Blob<T>::Blob(It b, It e):
      data(std::make_shared<std::vector<T>>(b, e)) {};

      // 实例化Blob<string>及其接收两个list<const char*>::iterator
      list<const char*> w = {"now", "is", "the"};
      Blob<string> a3(w.begin(), w.end());

控制实例化

  • 动机:在多个文件中实例化相同模板的额外开销可能非常严重。
  • 显式实例化:
    • extern template declaration; // 实例化声明 此声明必须出现在任何使用此实例化版本的代码之前
    • template declaration; // 实例化定义

效率与灵活性

在运行时绑定删除器

  • 很容易地重载一个shared_ptr删除器,只要在创建或reset指针时传递给它一个可调用对象即可,即删除器必须保存为一个指针或一个封装了指针的类。
  • 可以确定shared_ptr不是将删除器直接保存为一个成员,因为删除器的类型直到运行时才会知道,而shared_ptr的生存期中可以随时改变删除器的类型,类成员的类型在运行时是不能改变的。因此,不能直接保存删除器。

在编译时绑定删除器

  • 删除器的类型是一个unique_ptr对象类型的一部分,用户必须在定义unique_ptr时以显式模板实参的形式提供删除器的类型。
  • 删除器可以直接保存在unique_ptr对象中,删除器在编译时绑定,无运行时额外开销。

练习16.28

Q: 编写你自己版本的 shared_ptrunique_ptr

A: TODO

练习16.31

Q: 如果我们将 DebugDeleteunique_ptr 一起使用,解释编译器将删除器处理为内联形式的可能方式。

A: shared_ptr 是运行时绑定删除器,unique_ptr 则是编译时绑定删除器。unique_ptr 有两个模板参数,一个是所管理的对象类型,另一个是删除器类型。因此,删除器类型是 unique_ptr 类型的一部分,在编译时就可知道,删除器可直接保存在 unique_ptr 对象中。通过这种方式,unique_ptr 避免了间接调用删除器的运行时开销,而编译时还可以将自定义的删除器,如 DebugDelete 编译为内联形式。

模板实参推断

  • 对函数模板,编译器利用调用中的函数实参来确定其模板参数,这个过程叫模板实参推断

类型转换与模板类型参数

  • 能够自动转换类型的只有:
    • 和其他函数一样,顶层const会被忽略。
      顶层 const 无论出现在形参还是实参中都会被忽略。
    • 数组实参或函数实参转换为指针。
  • 其他类型,如算术转换、派生类向基类的转换以及用户自定义的转换都不能用于函数模板。
1
2
3
4
5
6
template <typename T> T fobj(T, T);
template <typename T> T fref(const T&, const T&);

int a[10], b[43];
fobj(a, b); // 调用f(int*, int*)
fref(a, b); // 错误:数组类型不匹配
  • 两个数组大小不同,因此是不同类型。
    • fobj 调用中,数组大小不同无关紧要,数组都被转换为指针。
    • fref 调用不合法,如果形参是一个引用,则数组不会转换为指针。

函数模板显式实参

  • 某些情况下,编译器无法推断出模板实参的类型。
  • 定义:template <typename T1, typename T2, typename T3> T1 sum(T2, T3);
  • 使用函数显式实参调用auto val3 = sum<long long>(i, lng); // T1是显式指定,T2和T3都是从函数实参类型推断而来
  • 注意:正常类型转换可以应用于显式指定的实参。

练习16.37

Q:标准库 max 函数有两个参数,它返回实参中的较大者。此函数有一个模版类型参数。你能在调用 max 时传递给它一个 int 和一个 double?如果可以,如何做?如果不可以,为什么?

A:可以。提供显式的模版实参即可:

1
2
3
int a = 1;
double b = 2;
std::max<double>(a, b);

练习16.38

Q:当我们调用 make_shared 时,必须提供一个显示模版实参。解释为什么需要显式模版实参以及它是如果使用的。

A:如果不显式提供模版实参,那么 make_shared 无法推断要分配多大内存空间。

练习16.39

Q:对16.1.1节 中的原始版本的 compare 函数,使用一个显式模版实参,使得可以向函数传递两个字符串字面量。

A:

1
compare<std::string>("hello", "world");

尾置返回类型与类型转换

  • 使用场景:并不清楚返回结果的准确类型,但知道所需类型是和参数相关的。
  • template <typename It> auto fcn(It beg, It end) -> decltype(*beg)
    解引用运算符返回一个左值,因此通过 decltype 推断的类型为 beg 表示的元素的类型的引用。
  • 尾置返回允许我们在参数列表之后声明返回类型。

标准库的类型转换模板:

  • 定义在头文件type_traits中。
  • 必须再返回类型的声明中使用 typename 来告知编译器,type 表示一个类型
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 unsigned X X
否则 T
make_unsigned 带符号类型 unsigned X
否则 T
remove_extent X[n] X
否则 T
remove_all_extents X[n1][n2]... X
否则 T

注意:add_rvalue_referenceX&X&& 得到 T

练习16.40

Q:下面的函数是否合法?如果不合法,为什么?如果合法,对可以传递的实参类型有什么限制(如果有的话)?返回类型是什么?

1
2
3
4
5
template <typename It>
auto fcn3(It beg, It end) -> decltype(*beg + 0) {
//处理序列
return *beg;
}

A:合法,但存在两个问题:

  1. 序列元素类型必须支持+运算符;
  2. *beg + 0 是右值,因此fcn3的返回类型被推断为元素类型的常量引用。

练习16.41

Q:编写一个新的sum版本,它的返回类型保证足够大,足以容纳加法结果。

A:注意:1.1的类型为double1.1f的类型为float

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
#include <iostream>
#include <climits>

using namespace std;

template <typename T1, typename T2>
auto sum(T1 lhs, T2 rhs) -> decltype(lhs + rhs) {
return lhs + rhs;
}

int main() {
auto a = sum(1, 1);
cout << a << " " << sizeof(a) << endl;
auto b = sum(1, 1.1);
cout << b << " " << sizeof(b) << endl;
auto c = sum(1, 1.1f);
cout << c << " " << sizeof(c) << endl;
std::cout << INT_MAX + INT_MAX << endl;
return 0;
}

// output
2 4
2.1 8
2.1 4
-2

函数指针和实参推断

  • 当使用一个函数模板初始化一个函数指针或为一个函数指针赋值时,编译器使用指针的类型来推断模板实参。

模板实参推断和引用

  • 从左值引用函数推断类型:若形如T&,则只能传递给它一个左值。但如果是const T&,则可以接受一个右值
  • 从右值引用函数推断类型:若形如T&&,则只能传递给它一个右值。
  • 引用折叠和右值引用参数:
    • 规则1:当我们将一个左值传递给函数的右值引用参数,且右值引用指向模板类型参数时(如T&&),编译器会推断模板类型参数为实参的左值引用类型。
    • 规则2:如果我们间接创造一个引用的引用,则这些引用形成了折叠。折叠引用只能应用在间接创造的引用的引用,如类型别名或模板参数。对于一个给定类型X
      • X& &X& &&X&& &都折叠成类型X&
      • 类型X&& &&折叠成X&&
    • 上面两个例外规则导致两个重要结果:
      • 1.如果一个函数参数是一个指向模板类型参数的右值引用(如T&&),则它可以被绑定到一个左值上;
      • 2.如果实参是一个左值,则推断出的模板实参类型将是一个左值引用,且函数参数将被实例化为一个左值引用参数(T&)。
  • 右值引用在涉及引用类型时,编写正确的代码会变得异常困难。
    右值引用通常用于两种情况:模板转发 和 模板被重载。模板被重载的例子如下(这种写法在P481可见):
    • template <typename T> void f(T&&); 绑定到非 const 右值
    • template <typename T> void f(const T&); 左值和 const 右值

练习16.42

个人感觉这道题很重要,理解清楚这个问题是后面理解为啥 std:moveremove_reference 的基础。

Q:对下面每个调用,确定 Tval 的类型:

1
2
3
4
5
6
template <typename T> void g(T&& val);
int i = 0;
const int ci = i;
(a) g(i);
(b) g(ci);
(c) g(i * ci);

A:

  1. Tint &val 的类型为 int &
    根据右值引用的特殊类型推断规则:当实参是一个左值时,编译器推断 T 为实参的左值引用类型,而非左值类型。而 int& && 在引用折叠规则的作用下,被折叠为int &
  2. Tconst int &val 的类型为 const int &
    原因同上。
  3. Tintval 的类型为 int &&
    实参是一个右值,编译器推断 T 为该右值的类型,因此 val 的类型就是右值类型的右值引用,被折叠为int &&

如何验证答案?

  • 可以在 g 中声明类型为 T 的局部变量v, 将val 赋值给它。然后打印 vval 的地址,即可判断 Tint & 还是 int
  • 还可通过对 v 赋值来判断 T 是否为 const的。

练习16.43

Q:使用上一题定义的函数,如果我们调用g(i = ci),g 的模版参数将是什么?

A:i = ci 返回的是左值,因此 g 的模版参数是 int&

练习16.44

Q:使用与第一题中相同的三个调用,如果 g 的函数参数声明为 T(而不是T&&),确定T的类型。如果g的函数参数是 const T&呢?

A:

  • 当声明为T的时候,T的类型都为intval的类型也都为int

  • 当声明为const T&的时候,T的类型都为int, val的类型都为const int &

练习16.45

Q:如果下面的模版,如果我们对一个像42这样的字面常量调用g,解释会发生什么?如果我们对一个int 类型的变量调用g 呢?

1
template <typename T> void g(T&& val) { vector<T> v; }

A:

  • g(42)T 被推断为intval 的类型为 int &&,因此 vint 的 vector。
  • g(i)T 被推断为 int&val 的类型折叠为 int &,因此v 被声明为 int & 的vector。
    但注意vector在动态内存空间中保存元素,需要维护指向其元素的指针,但引用不是对象,没有实际地址,因此不能定义指向引用的指针,也就是说vector<T&>会导致编译失败。

理解std::move

  • 标准库 move 函数是使用右值引用的模板的一个很好的例子。
  • 从一个左值 static_cast 到一个右值引用是允许的。
1
2
3
4
template <typename T>
typename remove_reference<T>::type&& move(T&& t) {
return static_cast<typename remove_reference<T>::type&&>(t);
}

必须要这么写,否则传左值引用时,T被推断为type&,根据引用折叠规则,返回的也是左值。

转发

  • 通过将一个函数参数定义为一个指向函数模板类型参数的右值引用,可以保持其对于实参的所有类型信息。
    使用引用类型(左值 / 右值)使得我们可以保持 const 属性,因为在引用类型中 const 的底层的。

  • 使用一个名为forward的新标准库设施来传递参数,它能够保持原始实参的类型

  • 注意:forward 不要用自动推导,要显式指定。

    forward需要把左值引用类型的左值转发为左值,右值引用类型的左值转发为右值,自动推导会一对一错。

  • 定义在头文件utility中。

  • 必须通过显式模板实参来调用。

  • forward返回显式实参类型的右值引用。即,forward<T>的返回类型是T&&

重载与模板

  • 多个可行模板:当有多个重载模板对一个调用提供同样好的匹配时,会选择最特例化的版本。
    比如 debug_rep(const T&)本质上可以用于任何类型,包括指针类型,其比 debug_rep(T*) 更通用,后者只能用于指针类型。
  • 非模板和模板重载:对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本

练习16.49

Q:解释下面每个调用会发生什么:

1
2
3
4
5
6
7
8
template <typename T> void f(T);
template <typename T> void f(const T*);
template <typename T> void g(T);
template <typename T> void g(T*);
int i = 42, *p = &i;
const int ci = 0, *p2 = &ci;
g(42); g(p); g(ci); g(p2);
f(42); f(p); f(ci); f(p2);

A:注意 f(p)f(int*)(其中T被实例化为int*)的匹配程度比和 f(const int*)(其中T被实例化为int)高,而 f(p2) 相反。

1
2
3
4
5
6
7
8
g(42);    	//g(T), T被推断为int
g(p); //g(T*), T被推断为int
g(ci); //g(T), T被推断为int
g(p2); //g(T*), T被推断位const int
f(42); //f(T), T被推断为int
f(p); //f(T), T被推断为int*
f(ci); //f(T), T被推断为int
f(p2); //f(const T*), T被推断为int

注意:形参中顶层const会被忽略(如3),而底层const不会(如果形参类型为T&, 则会保留 const 属性,因为引用类型中的 const 是底层的)。

可变参数模板

可变参数模板就是一个接受可变数目参数的模板函数或模板类。

  • 可变数目的参数被称为参数包
    • 模板参数包:标识l零个或多个模板参数。
    • 函数参数包:标识零个或多个函数参数。
  • 用一个省略号来指出一个模板参数或函数参数,表示一个包。
  • template <typename T, typename... Args>Args第一个模板参数包。
  • void foo(const T &t, const Args& ... rest);rest是一个函数参数包。
    如果一个参数的类型是一个模板参数包,则此参数也是一个函数参数包。
  • sizeof...运算符,返回参数的数目。
    类似 sizeofsizeof... 也返回一个常量表达式,而且不会对实参求值。

练习16.51

Q:调用本节中的每个 foo,确定 sizeof...(Args)sizeof...(rest)分别返回什么。

A:

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
#include <iostream>

using namespace std;

template <typename T, typename ... Args>
void foo(const T &t, const Args& ... rest){
cout << "sizeof...(Args): " << sizeof...(Args) << "\tsizeof...(rest): " << sizeof...(rest) << endl;
};

void test_param_packet(){
int i = 0;
double d = 3.14;
string s = "how now brown cow";

foo(i, s, 42, d);
foo(s, 42, "hi");
foo(d, s);
foo("hi");
foo(0, 1, 2, 3, 4, 5);
}

int main(){
test_param_packet();
return 0;
}

输出:

1
2
3
4
5
sizeof...(Args): 3	sizeof...(rest): 3
sizeof...(Args): 2 sizeof...(rest): 2
sizeof...(Args): 1 sizeof...(rest): 1
sizeof...(Args): 0 sizeof...(rest): 0
sizeof...(Args): 5 sizeof...(rest): 5

编写可变参数函数模板

  • 可变参数函数通常是递归的:第一步调用处理包中的第一个实参,然后用剩余实参调用自身。
  • 为了终止递归,还需要定义一个非可变参数的 print 函数。
    因为 可变模板 / 函数参数都可以表示零个参数。

练习16.53

Q:编写你自己版本的 print 函数,并打印一个、两个及五个实参来测试它,要打印的每个实参都应有不同的类型。

A:

1
2
3
4
5
6
7
8
9
template<typename Printable>
std::ostream& print(std::ostream& os, Printable const& printable) {
return os << printable;
}
// recursion
template<typename Printable, typename... Args>
std::ostream& print(std::ostream& os, Printable const& printable, Args const&... rest) {
return print(os << printable << ", ", rest...);
}

包扩展

  • 对于一个参数包,除了获取它的大小,唯一能做的事情就是 扩展(expand)
    扩展一个包就是把它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。通过再模式右边放一个省略号(…)来触发扩展操作。
  • 扩展一个包时,还要提供用于每个扩展元素的 模式(pattern)
1
2
3
4
5
6
7
template <typename T, typename... Args>
ostream &
print(ostream &os, const T &t, const Args&... rest) // 扩展Args
{
os << t << ", ";
return print(os, rest...); // 扩展rest
}

第一个扩展操作扩展模板参数包,将模式 const Args& 应用到模板参数包 Args 中的每个元素。此模式的扩展结果是一个逗号分隔的零个或多个类型的列表。

理解包扩展

  • print(os, debug_rep(res)...) 相当于 print(os, debug_rep(fcnNmae), debug_rep(code.num()), debug_rep(otherData), debug_rep("otherData"))
  • print(os, debug_rep(res...)) 相当于 print(os, debug_rep(fcnName, code.num(), otherData, "otherData")), 是错误的。

扩展中的模式会独立地应用于包中的每个元素。

转发参数包

  • 新标准下可以组合使用可变参数模板和 forward 机制,实现将实参不变地传递给其他函数。

  • 保持类型信息是一个两阶段的过程,

    • 首先,为了保持实参中的类型信息,必须将 emplace_back 的函数参数定义为模板类型参数的右值引用

      右值引用能接受任意类型参数,回忆右值引用参数规则和引用折叠规则。

    • 其次,当 emplace_back 将这些实参传递给 construct 时,必须使用 forward 来保持实参的原始类型信息。

  • alloc.construct(first_free++, std::forward<Args>(args)...) 其中std::forward<Args>(args)... 既扩展模板参数包 Args , 也扩展函数参数包args, 此模式生成的元素形式:std::forward<Ti>(ti)
    svec.emplace_back(10, 'c') 会扩展出 std::forward<int>(10), std::forward<char>(c)

建议:转发和可变参数模板

可变参数函数通常将它们的参数转发给其他函数。

1
2
3
4
5
template <typename... Args>
void fun(Args&&... args) { // 将Args扩展为一个右值引用的列表
// work的实参既扩展了Args又扩展了args
work(std::forward<Args>(args)...);
}

由于 fun 的参数是右值引用,因此我们可以传递给它任意类型的实参

思考:不能将一个右值引用直接绑定到一个变量上,那为啥可以对右值引用参数传递任意类型的实参呢?

回忆右值引用参数规则:形参是 T &&(T 是模板形参)或者 auto && 时,实参是右值时 T 不是引用(T && 是右值引用),实参是左值时 T 是左值引用。

注意:变量是左值喔!

重要扩展:完美转发

C++完美转发为什么必须要有std::forward?

参考:C++完美转发为什么必须要有std::forward? - Rogn - 博客园 (cnblogs.com)

1
2
3
4
5
template <typename T>
void G(T &&t) {
return F(t); // 1 direct call
// return F(std::forward<T>(t)); // 2 forward call
}

universal reference 转发时并不完美,只完美了一半,当转发目标的的参数是右值引用时,会出现问题:右值引用指向右值,但本身是左值
如果不采用转发,不管传进来的是右值,经过右值引用也会变成左值,从而去调用左值函数。

用forward(t)当实参,T推断成int&&,所以返回值是int&& &&,折叠成int&&,可以被绑定到F参数的右值引用上,就没有错误了。

std::forward的原理

引入右值引用就是为了避免不必要的拷贝和支持完美转发,这里很有必要介绍下完美转发。

参考:谈谈完美转发(Perfect Forwarding):完美转发 = 引用折叠 + 万能引用 + std::forward - 知乎 (zhihu.com)

forward 实现原理:

1
2
3
4
5
6
7
8
9
template <typename T>
T&& forward(typename std::remove_reference<T>::type& param) {
return static_cast<T&&>(param);
}

template <typename T>
T&& forward(typename std::remove_reference<T>::type&& param) {
return static_cast<T&&>(param);
}

紧接着std::forward模板函数对传入的参数进行强制类型转换,转换的目标类型符合引用折叠规则,因此左值参数最终转换后仍为左值,右值参数最终转成右值

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
33
34
35
36
37
#include <iostream>

template<typename T>
void print(T & t){
std::cout << "左值" << std::endl;
}

template<typename T>
void print(T && t){
std::cout << "右值" << std::endl;
}

template<typename T>
void testForward(T && v){
print(v);
print(std::forward<T>(v));
print(std::move(v));
}

int main(int argc, char * argv[])
{
testForward(1);

std::cout << "======================" << std::endl;

int x = 1;
testFoward(x);
}

//clang++ -std=c++11 -g -o forward test_forward.cpp
// 左值
// 右值
// 右值
// =========================
// 左值
// 左值
// 右值

练习16.58

Q:为你的 StrVec 类及你为16.1.2节练习中编写的 Vec 类添加 emplace_back 函数。

A:StrVec类:

1
2
3
4
5
6
template <class... Args>
inline
void StrVec::emplace_back(Args&&... args) {
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...);
}

Vec 类:

1
2
3
4
5
6
7
template<typename T>        //for the class  template
template<typename... Args> //for the member template
inline void
Vec<T>::emplace_back(Args&&...args) {
chk_n_alloc();
alloc.construct(first_free++, std::forward<Args>(args)...);
}

P596:当在类模板外定义一个成员模板时,必须同时为类模板和成员模板提供模板参数列表。
类模板的参数列表在前,后跟成员自己的模板参数列表。

练习16.60 & 16.61

Q:解释 make_shared 是如何工作的。并定义自己版本的make_shared

A:工作原理:接受参数包,经过扩展,转发给new作为vector的初始化参数。

1
2
3
4
template <typename T, typename... Args>
SP<T> make_shared(Args&&... args) {
return SP<T>(new T(std::forward<Args>(args)...));
}

TODO:SP 好像是在之前某次练习中定义的,但忘了在哪。

模板特例化(Specializations)

  • 定义函数模板特例化:关键字template后面跟一个空尖括号对(<>)。
  • 特例化的本质是实例化一个模板,而不是重载它。特例化不影响函数匹配。
  • 模板及其特例化版本应该声明在同一个头文件中。所有同名模板的声明应该放在前面,然后是特例化版本。
  • 我们可以部分特例化类模板,但不能部分特例化函数模板。