侯捷 C++ 11&14 新特性

2023/2/2

# 1.概述

包括语言和标准库两个部分

# 1.1 演化

  • C++ 98(1.0)
  • C++ 03(TR1,Technical Report 1)
  • C++ 11(2.0)
  • C++ 14

# 1.2 Header files

C++ 2.0新特性包括语言和标准库两个层面,后者以 header files 形式呈现

  • C++ 标准库的 header files 不带扩展名(.h),例如#include <vector>
  • 新式 C header files 不带副名称 .h,例如#include <cstdio>
  • 旧式 C header files(带有副名称.h)仍可用,例如#include <stdio.h>

曾经在 std::tr1 命名空间下的东西现在也都放到 std 里了,所以直接using namespace std;即可

# 1.3 重点

语言:

  • Variadic Templates
  • move Semantics
  • auto
  • Range-base for loop
  • Initializer list
  • Lambdas
  • ……

标准库:

  • type_traits
  • Unordered 容器
  • forward_list
  • array
  • tuple
  • Con-currency
  • RegEx
  • ……

# 2.Variadic Templates(可变参数模板)

模板的进化堪比原子弹

# 2.1 概述

模板 Templates:

  • 函数模板
  • 类模板

变化的是模板参数:

  • 参数个数:利用参数个数逐一递减的特性,实现递归函数的调用,使用函数模板完成
  • 参数类型:利用参数个数逐一递减以致参数类型也逐一递减的特性,实现递归继承或递归复合,以类模板完成
void print()
{
}
 
template <typename T, typename... Types>                //这里的...是关键字的一部分:模板参数包
void print(const T& firstArg, const Types&... args)     //这里的...要写在自定义类型Types后面:函数参数类型包
{
    cout << firstArg << endl;
    print(args...);                                     //这里的...要写在变量args后面:函数参数包
}
1
2
3
4
5
6
7
8
9
10
  • 参数的个数任意,参数的类型也任意
  • 传进来的参数会被分成一个和一包,如print(7.5, "hello", bitset<16>(377), 42);7.5 就是“一个”,后面的参数是“一包”,一个由 cout 输出,一包再递归处理。等一包里面内容为 0 时,就不能再调用这个函数了,所以上面又写了一个空参数的 print
  • 注意例子中...的位置,都是语法规则
  • 在可变模板参数内部可以使用sizeof...(args)得到实参的个数(即一包里面有多少个东西)
template <typename... Types>
void print(const Types&... args)
{/*......*/}
1
2
3
  • 如果还有这个,看起来会不能共存,因为有歧义,但其实可以
  • 后面会讲两个谁是泛化,谁是特化

image.png

  • hash 函数递归调用的例子
  • hash_val(c.fname, c.lname, c.no);的第一个参数不是size_t类型,所以会调用到 ①。① 给参数加上一个 seed 参数后传给 ②。② 先对 seed 调用 ④,再继续调用 seed 和参数,每次 ② 都会把参数拆成 1 个和多个,如果参数依然还剩多个就递归调用自己,只剩一个参数的话就调用 ③。

image.png

  • tuple 递归继承的例子
  • 关键在于private tuple<Tail...>,继承了那一包,然后再不停往上继承,最后继承自tuple<>

# 2.2 七大例子

在课程顺序上,这部分在 18 之后

image.png

  • 用一个模板函数接收各种各样的参数
  • ① 和 ③ 可以共存,因为 ① 更特化,但其实共存的时候 ③ 永远不会被调用。哪怕只剩 1 个了,也会调用 1+0 个,而不是 1 个。

image.png

  • 使用 variadic templates 模拟 C 的 printf()
  • 其实输出时没用到 %d 这些符号,直接往 cout 里丢,这几个 % 符号的作用主要是检查是不是数量相等,不相等就抛出异常

image.png

  • 可以接收任意参数的 max 函数,并没有用 variadic templates 而是用 initializer lists,放在这里的意思是,如果参数数量不限,但类型不一样,完全可以用 initializer lists
  • 从右上角开始看
  • 左侧箭头画错了,起点应该是下面的__max_element
  • _Iter_less_iter 是仿函数,用<比大小
  • 左侧 while 一个个比大小

image.png

  • 例 3 的改版,不再需要大括号
  • 上面的蓝色箭头画错了,应该指向下面的maximum()
  • 不断调用std::max()获取最大值
  • 当然,就像例 3 展示的,std::max()本来就可以接收任意数量的参数,只不过要加大括号

image.png

  • 用类模板把模板参数一个个分解
  • 我希望输出时对头尾元素做特殊操作,输出之后,前后会有中括号,中间用逗号间隔开。所以必须要知道当前处理的元素总共有几个(sizeof...()),以及现在处理的是第几个(通过 IDX 记录)
  • 通过 IDX+1 与 MAX 比较判断是不是最后一个,如果是就不额外打印东西了,如果不是就额外打印逗号。最后一个处理完了会进入到最下面的偏特化版本,什么都不做

image.png

  • tuple。前面都是递归调用、递归创建,而这个是递归继承
  • 递归继承,处理的是类型,要用类模板
  • “一个”拿来声明变量,“一包”再做成 tuple 被继承,非常巧妙
  • tail() 返回的是指针
  • tuple(Head v, Tail... vtail):m_head(v), inherited(vtail...){}inherited(vtail...)其实是调用父类构造函数完成初值设定
  • 但是typename Head::type head() {return m_head;}这行会报错,因为 int 和 float 都回答不了 type

image.png

  • 为了编译通过,想到可以用decltype得到 m_head 的 type
  • 又遇到了一个问题,此时 m_head 还没有出现,编译依然不通过,于是把 protect 挪上来

image.png

  • 后来发现其实是多此一举,刚才想的太复杂了
  • 其实根本不用问类型,就是 Head

image.png

  • 既然可以递归继承,那么也可以递归复合,模仿例 6 用复合实现 tuple

# 3.模板表达式中的空格

如果模板参数本身也是模板,尖括号之间必须要有空格:vector<list<int> >; 自 C++11 之后开始就可以去掉空格了:vector<list<int>>;

# 4.nullptr

标准库允许使用 nullptr 取代 0 或者 NULL 来对指针赋值(其实 NULL 就是 0)

  • nullptr 是个新关键字
  • nullptr 可以被自动转换为各种 pointer 类型,但不会被转换为任何整数类型
  • nullptr 的类型为 std::nullptr_t,定义于头文件中
void f(int);
void f(void*);

f(0);        // 调用 f(int).
f(NULL);     // 因为定义 NULL 为 0,所以调用 f(int),如果没有定义为 0,会产生二义性
f(nullptr);  // 调用 f(void*).
1
2
3
4
5
6

# 5.auto

auto 可以进行自动类型推导 注意,在 C 的语境下,auto 意味着局部变量,也叫 local 变量(因为函数结束后局部变量会自动消失) 使用 auto 的场景:类型太长(迭代器)或者类型太复杂(lambda)

vector<string> v;
auto pos = v.begin(); // 代替 vector<string>::iterator

auto l = [](int x)->bool{ // l 代表了 lambda

};
1
2
3
4
5
6

一种写法简化

list<string> c;
1ist<string>::iterator ite;
ite = find(c.begin(), c.end(), target)//现在可写为
list<string> c;
auto ite = find(c.begin(), c.end(), target)
1
2
3
4
5
6
7

# 6.uniform initialization(一致性初始化)

C++11 之前初始化时存在多个版本{}``()``=。让使用者使用时比较混乱

Rect r1 = {3, 7, 20, 25, &area, &print};
Rect r1(3, 7, 20, 25);
int ia[6] = {27, 210, 12, 47, 109, 83};
1
2
3

C++11 提供一种万用的初始化方法,任何初始化都可以使用大括号{}

int values[]{1, 2, 3};
vector<int> v {2, 3, 5, 7, 11, 13, 17};
vector<string> cities {
"Berlin","New York","London","Braunschweig","Cairo”,"Cologne"
};
complex<double> c{4.0, 3.0}; //相当于 c(4.0,3.0) 
1
2
3
4
5
6

只要变量名后接大括号就是初始化

原理解析:当编译器看到大括号包起来的东西{t1,t2...tn}时,会生成一个initializer_list<T>initializer_list关联至一个array<T,n>。调用函数(例如构造函数 ctor)时该 array 内的元素可被编译器分解逐一传给函数。 如vector<string> cities {"Berlin","New York","London","Braunschweig","Cairo”,"Cologne"};形成一个initializer_list<string>,背后有array<string,6>。调用vector<string>ctors 时编译器找到了一个vector<string> ctor 接受 initializer_list<string>complex<double> c{4.0, 3.0};形成一个initializer_list<double>,背后有array<double,2>。因为complex<double>并无任何 ctor 接受initializer_list<double>,所以调用complex<double> ctor 时该 array 内的 2 个元素被分解后传给 ctor。 但是如果调用函数自身提供了initializer_list<T>参数类型的构造函数,则不会分解而是直接传过去。直接整包传入进行初始化。所有的容器都可以接受这样的参数。

# 7.Initializer Lists

int i;      // 未初始化
int j{};    // j 初始化为 0
int* p;     // 未初始化
int* q{};   // q 初始化为 nullptr
1
2
3
4

大括号里面的内容不能窄化转换(narrowing conversion)

int x1(5.3);
int x2 = 5.3; //这俩可以,但会转换为 5
int x3{5.3};
int x4 = {5.3} //这俩会报错,因为大括号不能窄化转换
1
2
3
4

虽然文档上如此描述,但在实际测试当中,只会警告,不会报错

initializer_list 用来接收任意数量的东西{12,3,5,7,11,13,17}会自动被编译器当做std::initializer_list<int>。initializer_list是一个class(类模板),虽然数量任意,但类型要一致。

void print (std::initializer_list<int> vals)
{
for(auto p=vals.begin(); p!=vals.end(); ++p) { 
std::cout << *p <<"\n";
}
print({12,3,5,7,11,13,17});
1
2
3
4
5
6

image.png

  • p 是小括号,调用版本1;q 是大括号,调用版本 2(不要被箭头误导了,箭头指的是 complex 的情况);r 和 s 也都是一包,调用版本 2。
  • 如果没有 2,只有 1,p、q 和 s 会调用版本 1,但是 r 不行,因为 r 有三个参数,
  • 联系前面的例子,complex 就相当于没有版本 2,于是会调用到 1
  • 图右为 initializer_list 源码
    • initializer_list 背后有 array 数组支撑,initializer_list 关联一个 array<T,n>
    • initializer_list 只包含一个指向 array 的指针,而不是内含整个 array,它的拷贝只是一个浅拷贝,比较危险,两个指针指向同一个内存

根据源码检索,STL 的许多地方都用到了 initializer_list

  • 所有容器都接受指定任意数量的值用于构造或赋值或者 insert() 或 assign()
  • 算法 max() 和 min() 也接受任意参数

# 8.explict

image.png

  • C++11 之前的 explicit
  • 左侧成立,相加时编译器发现 5 可以完成转换,变成 5+0i
  • 如果你不想编译器完成这样的隐式转换,就加上 explicit,右侧会报错[Error] no match for 'operator+'(operand types are 'Complex' and 'int'
  • 但是 C++11 之前,只有非 explicit 的一个实参(one argument)的构造函数才会做隐式转换,而 C++11 开始多个实参也可以隐式转换了,所以 explicit 也支持多个参数的构造函数了

示例:

class P
{
public:
    P(int a, int b){
    	cout << "P(int a, int b) \n";
    }
    P(initializer_list<int>) {
    	cout << "P(initializer_list<int>) \n";
    }
    explicit P(int a, int b, int c) {
    cout << "explicit P(int a, int b, int c) \n";
    }
    };
void fp(const P&) {};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
P p1 (77, 5);		//P(int a, int b)
P p2 {77, 5};		//P(initializer_list<int>)
P p3 {77, 5, 42};	//P(initializer_list<int>)
P p4 = {77, 5};		//P(initializer_list<int>)
P p5 = {77, 5, 42}; //[Error] converting to 'P' from initializer list would use explicit constructor
p p6 (77, 5, 42);	//explict P(int a, int b, int c)
1
2
3
4
5
6

# 9.range-based for statement(基于范围的 for 循环)

// 例子
vector<double> vec;

for(auto elem:vec) {
	cout<<elem<<endl;
};  

for(auto& elem:  vec) {
    elem *= 3;
};    
1
2
3
4
5
6
7
8
9
10
  • 其实就是利用迭代器遍历了一遍

基于范围的 for 循环遇到类型不同时会做转换,如果之前用 explicit 禁止了转换,那么将会报错。

class C
{
	public:
	explicit C(const string& s);
};

vector<string> vs;
for(const C& elem : vs) { // ERROR, no conversion from string to C defined
cout << elem < endl;
}
1
2
3
4
5
6
7
8
9
10

# 10.= default, = delete

哪怕一个类是空的,C++ 也会给它默认的构造函数、拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数 如果已经自行定义了构造函数,那么编译器不会再给你默认的构造函数,可是如果加上=default,就可以重新获得并使用默认构造函数。=delete则相反,意味着这个函数已经删除,不能再被使用。引进这两种新特性的目的是为了增强对“类默认函数的控制”,从而让程序员更加精准地去控制默认版本的函数。

class Zoo
{
public:
    Zoo(int i1, int i2) : d1(i1), d2(i2){} //构造函数
    Zoo(const Zoo&) = delete; //拷贝构造 copy constructor
    Zoo(Zoo&&) = default; //移动构造 move constructor
    Zoo& operator=(const Zoo&)=default; //拷贝赋值 copy assignment
    Zoo& operator=(const Zoo&&)=delete; //移动赋值 move assignment
    virtual ~Zoo(){}

private:
	int d1, d2;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 构造函数、拷贝构造、拷贝赋值称为 big three,再加上移动构造和移动赋值是 big five

image.png

  • 我已经有一个构造函数了,默认的我也要,没问题,多个构造函数可以共存
  • 我已经写了拷贝构造,不能再=default了,因为拷贝构造只能有一个。=delete也不可以,既然已经写出来了又要 delete,那么编译器要何去何从呢?所以不行
  • 拷贝赋值同理,也只能有一个
  • func1()func2()是两个一般的函数。一般函数并没有=default版本,所以编译会报错,但是=delete没问题。不过这种用法其实挺少的,因为一般不想要哪个函数一开始就不写就好了
  • 析构函数先=delete=default也会报错,编译器搞不清你的意图
  • 补充:如果不想在某个类定义虚函数,可以加=0,代表纯虚函数。但=0只能用在虚函数上

什么样的类需要自己定义这些函数,而什么样的类用默认的就可以了呢?

  • 如果类中带有 pointer member(指针成员),那我们就可以断定必须要自己写出 big five; 如果不带,绝大多数情况下就不必给出 big five。
  • 复数类中没有指针,拷贝就是把实部、虚部都拷贝过去即可,编译器默认给的也是这么做的
  • 字符串中有指针,涉及到浅拷贝和深拷贝的问题,所以必须要自己写出 big five

image.png

  • 根据=default=delete写出这三个特别的类
  • NoCopy 不允许拷贝,所以把拷贝构造和拷贝赋值都=delete
  • NoDtor 删除了析构函数,这么做一定要考虑后果,对象离开作用域或消除时都会报错
  • PrivateCopy 不允许一般的代码拷贝,但是可以被友元或成员拷贝

image.png

  • boost 就使用了类似的手法
  • 它也把拷贝构造和拷贝赋值都放到 private 里了,如此设计是为了让其他的类来继承,继承之后也会具有这样的性质

# 11.Alias Template(模板别名)

template <typename T>
using Vec = std::vector<T, MyAlloc<T>>;

//使用
Vec<int> coll; //相当于 std::vector<int, MyAlloc<int>> coll;
1
2
3
4
5

使用宏无法达到相同的效果

#define Vec<T> template<typename T> std::vector<T, MyAlloc<T>>;

Vec<int> coll;
//相当于
template<typename int> std::vector<int, MyAlloc<int>> coll; //牛头不对马嘴
1
2
3
4
5

typedef 同样也不行,因为 typedef 无法制定参数

Alias Template 无法特化(偏特化和全特化都不行)。

image.png

  • 我希望写一个测试函数,可以传入任意的容器和类型
  • 左侧就是天方夜谭,因为函数传入的参数都是对象,这里却想拿 cntr 的参数类型做构造
  • 右侧就是传东西了(list()是个临时对象),然后用函数模板取出类型,但依然是天方夜谭,因为 Container 不是模板
template<typename Container>
void test_moveable(Container c)
{
    typedef typename iterator_traits<typename Container::iterator>::value_type Valtype;
    
    for(long i=0; i<SIZE; ++i)
        c.insert(c.end(), Valtype());
    
    output_static_data(*(c.begin()));
    Container<T> c1(c);
    Container<T> c2(std::move(c));
    c1.swap(c2);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
test moveable(list<MyString>());
test moveable(list<MyStrNoMov>());

test moveable(vector<MyString>());
test moveable(vector<MyStrNoMow>());

test moveable(deque<MyString>());
test moveable(deque<MyStrNoMove>());
1
2
3
4
5
6
7
8
  • 只好牺牲调用端的弹性,在调用端就把容器和类型结合起来
  • 因为传入的是结合后的,代码里必须要把元素类型取出来,通过把容器的迭代器丢到萃取机里得到元素类型,并重命名为 Valtype
  • 这样的写法并不差,但没有达到最初想要的效果。而且,如果容器没有迭代器或 traits 呢(当然标准库里的容器肯定都有)。该如何做到能接受一个模板参数 Container,而 Container 本身又是个模板,并从中取出 Container 的模板参数?如收到 vector,如何取出元素类型 string?

下面用 template template parameter(模板模板参数)解决这个问题

template <typename T,
          template <typename T>  //T 可写可不写,默认是前面的 T
              class Container
         >
class XCls
{
private:
    Container<T> c;
public:
    XCls() //构造函数
    {
        for(long i=0; i<SIZE; ++i)
            c.insert(c.end(), T());
 
        output_static_data(T());
        Container<T> c1(c);
        Container<T> c2(std::move(c));
        c1.swap(c2);
    }
};

// 使用时会报错
XCls<MyString, vector> c1;        //[Error] vector的实际类型和模板中的Container<T>类型不匹配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  • template <class> class Container是外面模板的模板参数,而它自己本身也是一个模板
  • XCls 这个名字是随便取的,意思是 X Class
  • 代码本身编译通过了,但加上使用的代码后报错了,因为 vector 是有两个参数的(元素类型和分配器),但代码里Container<T> c;只写了一个。平时写的时候因为分配器这个参数有默认值所以只需要写一个,但现在作为模板模板参数,编译器无法推导
  • 这时就引入我们的主题了:Alias Template
template<typename T>
using Vec = vector<T, allocator<T>>;
 
XCls<MyString, Vec> c1;
1
2
3
4

上面的代码都不变,在外部加上这三行,Vec 只需要一个参数就够了,与Container<T> c;相匹配,达到我们的目标 可见 Alias Template 并非只是让我们少写几行代码,而是有更丰富的作用

# 12.Type Alias

类似 typedef,同样借助 using 关键字来使用

using func = void(*)(int, int);
//相当于
typedef void (*func)(int, int);

void example(int, int){}
func fn = example;
1
2
3
4
5
6

func 是一个类型,typedef 的语法并不能很好的表达这一点,Type Alias 就清晰多了 Type Alias 和 typedef 完全等价

# 13.using 用法总结

  1. 打开命名空间(using namespace std)或者命名空间(using std::cout)的成员
  2. 打开类的成员(using _Base::_M_allocate;,这样以后只需写 _M_allocate,编译器便会到 _Base 类里去找)
  3. 类型别名和模板别名(C++ 11开始支持)

# 14.noexcept

image.png

  • 在函数后面加上noexpect关键字,保证这个函数不会丢出异常,后面可以在小括号里面写上不会丢出异常的限定条件,noexpect就相当于noexpect(true)
  • 一般异常处理流程:当程序发生异常时会将异常信息上报返回给调用者,如果有异常处理则处理,如果该调用者没有处理异常则会接着上报上一层,若到了最上层都没有处理,就会调用std::terminate()调用std::abort(),然后终止程序

image.png

  • 一定要给移动构造和移动赋值加上noexpect,vector 才会用它
  • 但实际写的时候不知道其他人会用什么,所以最好有移动构造和移动赋值就写上noexpect

# 15.override

image.png

  • override应用在虚函数上,告诉编译器这个函数就是要重写父类虚函数,让编译器帮忙检查
  • 上面框中,子类本来想重写父类的函数,结果写错了,编译器会认为这是子类的新函数
  • 下面框中,在子类后面加上override,编译器会报错,告诉你写错了

# 16.final

image.png

  • 有两个作用
    • 父类禁止自己被继承
    • 虚函数禁止自己被重写

# 17.decltype

decltype可以让编译器找出表达式的类型,虽然之前已经有了typeof,但它并非标准库的一部分,所以 C++ 11 加入了 decltype

map<string, float> coll;
decltype(coll)::value_type elem;
1
2
  • 实际当中这两行可能隔得很远。我们知道这个是容器,所以用 value_type 拿到类型,然后用它的 type 声明变量 elem
  • C++ 11 之前无法通过对象取得 type,你必须知道那个对象是什么类型,要写成map<string, float>::value_type elem;

三种应用:声明返回类型、模板之间应用(元编程)、求 lambda 表达式类型 image.png

  • 声明一种返回类型
  • 一般+都是作用在两个相同类型之间的,但这里 x 和 y 是两个类型。decltype的作用就体现出来了,它允许我们的返回类型是 x+y 之后的类型。但是这样编译不通过,因为 x 和 y 在后面才出现
  • 于是写成autoAdd(T1 x, T2 y) -> decltype(x + y);,意思一样,但可以通过编译。当然,即使编译通过了,如果用的时候 x 和 y 无法相加,还是会报错
  • 这种指定方式和 lambdas 很像

image.png

  • 模板之间的应用
  • 模板之间调来调去之后,如果我想知道 obj 的类型,就可以用decltype
  • 同理,虽然编译可以通过,如果使用者传进来一个没有迭代器的 obj,就会报错

image.png

  • 求 lambda 表达式类型
  • lambda 是一个函数,这个函数用 cmp 表示,如果只是用这个对象很好办,但有时候我们会需要它的类型,我们又不知道,就可以用decltype

# 18.Lambdas

image.png

  • lambda 是函数的定义,lambda 可以用作内联函数,可以被当做一个参数或者一个对象,类似于仿函数。
  • 中括号开头的就是 lambda,大括号内是函数本体,调用的时候直接在后面加小括号即可(注意,这里和前面的概念不同,小括号不是生成临时对象,而是直接调用)

image.png

  • 完整形式
    • []:lambda 导入器(introducer),看到它就知道是 lambda,取用外部变量
    • ():参数
    • mutable:[] 中的导入数据是否可变
    • throwSpec:抛出异常
    • retType:返回类型
    • {}:函数体
  • 因为 lambda 的写法是 [] 开始的,所以返回类型写到了后面
  • mutable、throwSpec、retType 都是可写可不写的,但只要写了三个里的其中一个,就必须也要写上小括号
  • [=, &y]=的意思是接收任意外界对象 by value。不是很推荐这种写法,可读性比较差

image.png

  • 左边 lambda 和右边是近乎等价的
  • 所以会有如此的输出结果,它变的不是外面的 id,而是传入后的自己的 id。这里是 pass by value
  • 但如果不写 mutable,确实不能++id,可见左右不完全相同

image.png

  • 左右都是 pass by value,中间是 pass by reference
  • 中间的变化会影响外界
  • 右框因为没写 mutable,所以不能++id
  • 左下框是可以编译通过的,lambda 可以有静态、非静态数据,也可以 return

image.png

  • ① 相当于 ②

image.png

  • 如果你需要 lambda 当做排序准则交给 set,就需要用decltype()获取它的类型。你也必须把 cmp(lambda obj)传给coll()的构造函数,否则coll()会调用默认构造函数,不幸的是,lambda 并没有默认构造函数,也没有赋值操作,所以会报错。因此,如果要写排序准则,最好写成仿函数,会更直观。

image.png

  • n 是元素,x y 是范围,我希望所有的元素都在 x<n<y 的范围里,把不符合范围的删掉。写成左侧的 lambda 要比右侧的仿函数简洁很多,而且因为 lambda 是 inline,效率也会略高

# 19.Rvalue references(右值引用)

上面都是语言,从这开始进入第二个部分:标准库 右值引用严格来说算是语言,但它和标准库有关 image.png

  • 帮助解决非必要的拷贝。如果赋值的右侧是个右值,那么左值就可以直接把右值移动(move)过来,而不用重新分配
  • 变量就是左值,不能放在左边的就是右值
  • 例子中ab都是左值,a+b就是右值,临时对象也是右值(可以理解成临时对象没有名字,没法赋值)
  • string 和 complex 比较特别,没有遵循定义,不用管它们

image.png

  • &foo可以,对函数取地址。&foo()不行,因为函数返回的东西是右值,不能对右值取地址。C++ 11 之后就不一样了

image.png

  • Vtype(buf)是临时对象,自然而然是右值
  • 过去insert()为了 copy 会调用这个元素的拷贝构造,现在已经明确自己是可以 move 的了,于是会调用移动构造。前者是深拷贝,后者是浅拷贝。所以右值在 move 之后就不能再用了,不然会有危险
  • 如果我是左值,但我明确我以后不会再用了,也想 move,只要把左值放到std::move()里即可,就能得到左值的右值版本

image.png

  • G2.9 (代表 C++ 11 之前)中 vector 的 insert 只有一个版本,G4.9(代表 C++ 11 之后)则有两个版本

image.png

  • 不但有移动构造,也有移动赋值

# 20.Perfect Forwarding

image.png

  • 先看看什么是不完美的传递
  • 两个process(),分别是左值版本和右值版本,通过输出可以知道调用了哪个版本
  • 中介函数forward()forward()再调用process()。这时候出现了问题,右值经过转交后被当成了左值处理
  • 左值更不用说了,完全不能调用forward()

image.png image.png

  • 标准库中的forward()可以做到完美传递,我们不深究实现细节

# 21.move aware class

image.png

  • 写一个可以被 move 的 class,作为元素的类型。未来容器需要的时候就可以 move 它而不是拷贝
  • 重点是 move constructor。首先参数是右值引用,然后把指针和长度设过来,注意,清除动作(delete)不应该写在这,而是应该交给析构函数来做。一定要把指针设为 NULL,即把指针打断,析构函数也要检查是不是 NULL,不然会把数据本身杀掉

image.png

  • 接上页
  • 移动赋值其他和拷贝赋值都一样,只不过是浅拷贝而不是深拷贝
  • 测试时要把它放到各种容器里,而关联式容器如 set、map 需要检测元素大小,所以也要重载<==,因为 MyString 是 C 风格字符串,为了方便,借用 C++ 的 String 比大小。不过这部分和 move 无关,只是为了测试。同理,hash table 需要 hash function,所以写了蓝色的部分

image.png image.png image.png image.png image.png image.png

  • 与《STL 标准库与泛型编程》中的 9.5 部分相同

image.png

  • 为什么 copy 和 move 速度差这么多?
  • vector 的拷贝构造是真的一个个的把来源端拷贝到目的端。copy 最终的确完成了 memory allocation 及 copy ctor 的调用
  • 顺着箭头走,它最后所做的事只是 swap 来源端和目的端的三根指针。move-copy 最终是三根指针进行 swap(),交换后 c 变成了 c2,而 c2 变成了 c,但 c2 原本无意义,所以现在 c 变得无意义,因此 move-copy 之后的来源端(本例的 c)不能再被使用,否则后果自负

# 22.容器

# 22.1 array

与《STL 标准库与泛型编程》中的 6.4 部分相同。本质是数组,给数组容器该有的接口。

# 22.2 Hash Table

# 22.2.1 容器 hash table

与《STL 标准库与泛型编程》中的 6.7.1 部分类似。主要讲了 Separate Chaining(分离链接法)

# 22.2.2 hash function

image.png

  • 测试基本类型数据的 hash code 怎么取
  • 整数型的数据得到的也是整数型(包括 char)
  • 都是 3.141592653,float 和 double 不一样;都是 Ace,C 的字符串和 C++ 的不一样

image.png

  • G2.9 整形的哈希函数,模板特化,传入什么就返回什么

image.png

  • G2.9 字符串的哈希函数(C 风格的字符串,C++ 的字符串在 G2.9 还没有)

image.png

  • 手动计算出的结果与应该落入的桶的编号与观察到的结果相匹配

image.png

  • G4.9 版的哈希函数
  • 针对各种各样的类型做特化

image.png

  • 这一页左侧和上一页右侧都是针对整形做特化

image.png

  • 针对浮点数做特化,调用_Hash_impl::hash()
  • 根据上一页,_Hash_impl::hash()调用了_Hash_bytes(),但这个函数没有定义,只有声明(可能已经编译成二进制了),所以如何计算浮点数的哈希函数不得而知

image.png

  • functional_hash.h 里并没有字符串相关的哈希函数,很合理,因为字符串应该自己设计自己的哈希函数

image.png

  • 字符串的哈希函数,最后也是落到不知道怎么实现的_Hash_bytes()

image.png

  • _Hash_bytes()出现的地方,只有声明,没有定义

一个万用的哈希函数,该部分没有视频,推断与《STL 标准库与泛型编程》中的 9.1 部分类似。用到了 variadic templates

# 23.tuple

image.png

  • tuple 源码大家已经很熟悉了(前面的 variadic templates 讲的很透彻了),这里只看用法
  • 与《STL 标准库与泛型编程》中的 9.2 部分相同

image.png

  • 在没有 variadic templates 的时候也有 tuple,我们看一下当时的做法
  • GenScatterHiderarchy其实就是想实现左侧的继承体系,它用 #define 硬写出来,局限在于它写到多少,里面就只能放多少个东西

image.png

  • boost 里的名字就叫 tuple
  • 概念和上面的差不多,也是硬写出来的,最多写到了针对 15 个的
  • tuple_base这一页放不开,放在上一页

其他参考:

#

#