# 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后面:函数参数包
}
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)
{/*......*/}
2
3
- 如果还有这个,看起来会不能共存,因为有歧义,但其实可以
- 后面会讲两个谁是泛化,谁是特化
- hash 函数递归调用的例子
hash_val(c.fname, c.lname, c.no);
的第一个参数不是size_t
类型,所以会调用到 ①。① 给参数加上一个 seed 参数后传给 ②。② 先对 seed 调用 ④,再继续调用 seed 和参数,每次 ② 都会把参数拆成 1 个和多个,如果参数依然还剩多个就递归调用自己,只剩一个参数的话就调用 ③。
- tuple 递归继承的例子
- 关键在于
private tuple<Tail...>
,继承了那一包,然后再不停往上继承,最后继承自tuple<>
# 2.2 七大例子
在课程顺序上,这部分在 18 之后
- 用一个模板函数接收各种各样的参数
- ① 和 ③ 可以共存,因为 ① 更特化,但其实共存的时候 ③ 永远不会被调用。哪怕只剩 1 个了,也会调用 1+0 个,而不是 1 个。
- 使用 variadic templates 模拟 C 的 printf()
- 其实输出时没用到 %d 这些符号,直接往 cout 里丢,这几个 % 符号的作用主要是检查是不是数量相等,不相等就抛出异常
- 可以接收任意参数的 max 函数,并没有用 variadic templates 而是用 initializer lists,放在这里的意思是,如果参数数量不限,但类型不一样,完全可以用 initializer lists
- 从右上角开始看
- 左侧箭头画错了,起点应该是下面的
__max_element
- _Iter_less_iter 是仿函数,用
<
比大小 - 左侧 while 一个个比大小
- 例 3 的改版,不再需要大括号
- 上面的蓝色箭头画错了,应该指向下面的
maximum()
- 不断调用
std::max()
获取最大值 - 当然,就像例 3 展示的,
std::max()
本来就可以接收任意数量的参数,只不过要加大括号
- 用类模板把模板参数一个个分解
- 我希望输出时对头尾元素做特殊操作,输出之后,前后会有中括号,中间用逗号间隔开。所以必须要知道当前处理的元素总共有几个(
sizeof...()
),以及现在处理的是第几个(通过 IDX 记录) - 通过 IDX+1 与 MAX 比较判断是不是最后一个,如果是就不额外打印东西了,如果不是就额外打印逗号。最后一个处理完了会进入到最下面的偏特化版本,什么都不做
- 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
- 为了编译通过,想到可以用
decltype
得到 m_head 的 type - 又遇到了一个问题,此时 m_head 还没有出现,编译依然不通过,于是把 protect 挪上来
- 后来发现其实是多此一举,刚才想的太复杂了
- 其实根本不用问类型,就是 Head
- 既然可以递归继承,那么也可以递归复合,模仿例 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*).
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
};
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);
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};
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)
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
2
3
4
大括号里面的内容不能窄化转换(narrowing conversion)
int x1(5.3);
int x2 = 5.3; //这俩可以,但会转换为 5
int x3{5.3};
int x4 = {5.3} //这俩会报错,因为大括号不能窄化转换
2
3
4
虽然文档上如此描述,但在实际测试当中,只会警告,不会报错
initializer_list 用来接收任意数量的东西{12,3,5,7,11,13,17}
会自动被编译器当做std::initializer_list<int>
。initializer_list
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});
2
3
4
5
6
- 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,它的拷贝只是一个浅拷贝,比较危险,两个指针指向同一个内存
- initializer_list
根据源码检索,STL 的许多地方都用到了 initializer_list
- 所有容器都接受指定任意数量的值用于构造或赋值或者 insert() 或 assign()
- 算法 max() 和 min() 也接受任意参数
# 8.explict
- 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&) {};
};
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)
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;
};
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;
}
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;
};
2
3
4
5
6
7
8
9
10
11
12
13
- 构造函数、拷贝构造、拷贝赋值称为 big three,再加上移动构造和移动赋值是 big five
- 我已经有一个构造函数了,默认的我也要,没问题,多个构造函数可以共存
- 我已经写了拷贝构造,不能再
=default
了,因为拷贝构造只能有一个。=delete
也不可以,既然已经写出来了又要 delete,那么编译器要何去何从呢?所以不行 - 拷贝赋值同理,也只能有一个
func1()
和func2()
是两个一般的函数。一般函数并没有=default
版本,所以编译会报错,但是=delete
没问题。不过这种用法其实挺少的,因为一般不想要哪个函数一开始就不写就好了- 析构函数先
=delete
再=default
也会报错,编译器搞不清你的意图 - 补充:如果不想在某个类定义虚函数,可以加
=0
,代表纯虚函数。但=0
只能用在虚函数上
什么样的类需要自己定义这些函数,而什么样的类用默认的就可以了呢?
- 如果类中带有 pointer member(指针成员),那我们就可以断定必须要自己写出 big five; 如果不带,绝大多数情况下就不必给出 big five。
- 复数类中没有指针,拷贝就是把实部、虚部都拷贝过去即可,编译器默认给的也是这么做的
- 字符串中有指针,涉及到浅拷贝和深拷贝的问题,所以必须要自己写出 big five
- 根据
=default
和=delete
写出这三个特别的类 - NoCopy 不允许拷贝,所以把拷贝构造和拷贝赋值都
=delete
了 - NoDtor 删除了析构函数,这么做一定要考虑后果,对象离开作用域或消除时都会报错
- PrivateCopy 不允许一般的代码拷贝,但是可以被友元或成员拷贝
- boost 就使用了类似的手法
- 它也把拷贝构造和拷贝赋值都放到 private 里了,如此设计是为了让其他的类来继承,继承之后也会具有这样的性质
# 11.Alias Template(模板别名)
template <typename T>
using Vec = std::vector<T, MyAlloc<T>>;
//使用
Vec<int> coll; //相当于 std::vector<int, MyAlloc<int>> coll;
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; //牛头不对马嘴
2
3
4
5
typedef 同样也不行,因为 typedef 无法制定参数
Alias Template 无法特化(偏特化和全特化都不行)。
- 我希望写一个测试函数,可以传入任意的容器和类型
- 左侧就是天方夜谭,因为函数传入的参数都是对象,这里却想拿 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);
}
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>());
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>类型不匹配
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;
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;
2
3
4
5
6
func 是一个类型,typedef 的语法并不能很好的表达这一点,Type Alias 就清晰多了 Type Alias 和 typedef 完全等价
# 13.using 用法总结
- 打开命名空间(
using namespace std
)或者命名空间(using std::cout
)的成员 - 打开类的成员(
using _Base::_M_allocate;
,这样以后只需写 _M_allocate,编译器便会到 _Base 类里去找) - 类型别名和模板别名(C++ 11开始支持)
# 14.noexcept
- 在函数后面加上
noexpect
关键字,保证这个函数不会丢出异常,后面可以在小括号里面写上不会丢出异常的限定条件,noexpect
就相当于noexpect(true)
- 一般异常处理流程:当程序发生异常时会将异常信息上报返回给调用者,如果有异常处理则处理,如果该调用者没有处理异常则会接着上报上一层,若到了最上层都没有处理,就会调用
std::terminate()
调用std::abort()
,然后终止程序
- 一定要给移动构造和移动赋值加上
noexpect
,vector 才会用它 - 但实际写的时候不知道其他人会用什么,所以最好有移动构造和移动赋值就写上
noexpect
# 15.override
override
应用在虚函数上,告诉编译器这个函数就是要重写父类虚函数,让编译器帮忙检查- 上面框中,子类本来想重写父类的函数,结果写错了,编译器会认为这是子类的新函数
- 下面框中,在子类后面加上
override
,编译器会报错,告诉你写错了
# 16.final
- 有两个作用
- 父类禁止自己被继承
- 虚函数禁止自己被重写
# 17.decltype
decltype
可以让编译器找出表达式的类型,虽然之前已经有了typeof
,但它并非标准库的一部分,所以 C++ 11 加入了 decltype
map<string, float> coll;
decltype(coll)::value_type elem;
2
- 实际当中这两行可能隔得很远。我们知道这个是容器,所以用 value_type 拿到类型,然后用它的 type 声明变量 elem
- C++ 11 之前无法通过对象取得 type,你必须知道那个对象是什么类型,要写成
map<string, float>::value_type elem;
三种应用:声明返回类型、模板之间应用(元编程)、求 lambda 表达式类型
- 声明一种返回类型
- 一般
+
都是作用在两个相同类型之间的,但这里 x 和 y 是两个类型。decltype
的作用就体现出来了,它允许我们的返回类型是 x+y 之后的类型。但是这样编译不通过,因为 x 和 y 在后面才出现 - 于是写成
autoAdd(T1 x, T2 y) -> decltype(x + y);
,意思一样,但可以通过编译。当然,即使编译通过了,如果用的时候 x 和 y 无法相加,还是会报错 - 这种指定方式和 lambdas 很像
- 模板之间的应用
- 模板之间调来调去之后,如果我想知道 obj 的类型,就可以用
decltype
- 同理,虽然编译可以通过,如果使用者传进来一个没有迭代器的 obj,就会报错
- 求 lambda 表达式类型
- lambda 是一个函数,这个函数用 cmp 表示,如果只是用这个对象很好办,但有时候我们会需要它的类型,我们又不知道,就可以用
decltype
# 18.Lambdas
- lambda 是函数的定义,lambda 可以用作内联函数,可以被当做一个参数或者一个对象,类似于仿函数。
- 中括号开头的就是 lambda,大括号内是函数本体,调用的时候直接在后面加小括号即可(注意,这里和前面的概念不同,小括号不是生成临时对象,而是直接调用)
- 完整形式
- []:lambda 导入器(introducer),看到它就知道是 lambda,取用外部变量
- ():参数
- mutable:[] 中的导入数据是否可变
- throwSpec:抛出异常
- retType:返回类型
- {}:函数体
- 因为 lambda 的写法是 [] 开始的,所以返回类型写到了后面
- mutable、throwSpec、retType 都是可写可不写的,但只要写了三个里的其中一个,就必须也要写上小括号
[=, &y]
中=
的意思是接收任意外界对象 by value。不是很推荐这种写法,可读性比较差
- 左边 lambda 和右边是近乎等价的
- 所以会有如此的输出结果,它变的不是外面的 id,而是传入后的自己的 id。这里是 pass by value
- 但如果不写 mutable,确实不能
++id
,可见左右不完全相同
- 左右都是 pass by value,中间是 pass by reference
- 中间的变化会影响外界
- 右框因为没写 mutable,所以不能
++id
- 左下框是可以编译通过的,lambda 可以有静态、非静态数据,也可以 return
- ① 相当于 ②
- 如果你需要 lambda 当做排序准则交给 set,就需要用
decltype()
获取它的类型。你也必须把 cmp(lambda obj)传给coll()
的构造函数,否则coll()
会调用默认构造函数,不幸的是,lambda 并没有默认构造函数,也没有赋值操作,所以会报错。因此,如果要写排序准则,最好写成仿函数,会更直观。
- n 是元素,x y 是范围,我希望所有的元素都在 x<n<y 的范围里,把不符合范围的删掉。写成左侧的 lambda 要比右侧的仿函数简洁很多,而且因为 lambda 是 inline,效率也会略高
# 19.Rvalue references(右值引用)
上面都是语言,从这开始进入第二个部分:标准库
右值引用严格来说算是语言,但它和标准库有关
- 帮助解决非必要的拷贝。如果赋值的右侧是个右值,那么左值就可以直接把右值移动(move)过来,而不用重新分配
- 变量就是左值,不能放在左边的就是右值
- 例子中
a
、b
都是左值,a+b
就是右值,临时对象也是右值(可以理解成临时对象没有名字,没法赋值) - string 和 complex 比较特别,没有遵循定义,不用管它们
&foo
可以,对函数取地址。&foo()
不行,因为函数返回的东西是右值,不能对右值取地址。C++ 11 之后就不一样了
Vtype(buf)
是临时对象,自然而然是右值- 过去
insert()
为了 copy 会调用这个元素的拷贝构造,现在已经明确自己是可以 move 的了,于是会调用移动构造。前者是深拷贝,后者是浅拷贝。所以右值在 move 之后就不能再用了,不然会有危险 - 如果我是左值,但我明确我以后不会再用了,也想 move,只要把左值放到
std::move()
里即可,就能得到左值的右值版本
- G2.9 (代表 C++ 11 之前)中 vector 的 insert 只有一个版本,G4.9(代表 C++ 11 之后)则有两个版本
- 不但有移动构造,也有移动赋值
# 20.Perfect Forwarding
- 先看看什么是不完美的传递
- 两个
process()
,分别是左值版本和右值版本,通过输出可以知道调用了哪个版本 - 中介函数
forward()
,forward()
再调用process()
。这时候出现了问题,右值经过转交后被当成了左值处理 - 左值更不用说了,完全不能调用
forward()
- 标准库中的
forward()
可以做到完美传递,我们不深究实现细节
# 21.move aware class
- 写一个可以被 move 的 class,作为元素的类型。未来容器需要的时候就可以 move 它而不是拷贝
- 重点是 move constructor。首先参数是右值引用,然后把指针和长度设过来,注意,清除动作(delete)不应该写在这,而是应该交给析构函数来做。一定要把指针设为 NULL,即把指针打断,析构函数也要检查是不是 NULL,不然会把数据本身杀掉
- 接上页
- 移动赋值其他和拷贝赋值都一样,只不过是浅拷贝而不是深拷贝
- 测试时要把它放到各种容器里,而关联式容器如 set、map 需要检测元素大小,所以也要重载
<
和==
,因为 MyString 是 C 风格字符串,为了方便,借用 C++ 的 String 比大小。不过这部分和 move 无关,只是为了测试。同理,hash table 需要 hash function,所以写了蓝色的部分
- 与《STL 标准库与泛型编程》中的 9.5 部分相同
- 为什么 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
- 测试基本类型数据的 hash code 怎么取
- 整数型的数据得到的也是整数型(包括 char)
- 都是 3.141592653,float 和 double 不一样;都是 Ace,C 的字符串和 C++ 的不一样
- G2.9 整形的哈希函数,模板特化,传入什么就返回什么
- G2.9 字符串的哈希函数(C 风格的字符串,C++ 的字符串在 G2.9 还没有)
- 手动计算出的结果与应该落入的桶的编号与观察到的结果相匹配
- G4.9 版的哈希函数
- 针对各种各样的类型做特化
- 这一页左侧和上一页右侧都是针对整形做特化
- 针对浮点数做特化,调用
_Hash_impl::hash()
- 根据上一页,
_Hash_impl::hash()
调用了_Hash_bytes()
,但这个函数没有定义,只有声明(可能已经编译成二进制了),所以如何计算浮点数的哈希函数不得而知
- functional_hash.h 里并没有字符串相关的哈希函数,很合理,因为字符串应该自己设计自己的哈希函数
- 字符串的哈希函数,最后也是落到不知道怎么实现的
_Hash_bytes()
_Hash_bytes()
出现的地方,只有声明,没有定义
一个万用的哈希函数,该部分没有视频,推断与《STL 标准库与泛型编程》中的 9.1 部分类似。用到了 variadic templates
# 23.tuple
- tuple 源码大家已经很熟悉了(前面的 variadic templates 讲的很透彻了),这里只看用法
- 与《STL 标准库与泛型编程》中的 9.2 部分相同
- 在没有 variadic templates 的时候也有 tuple,我们看一下当时的做法
GenScatterHiderarchy
其实就是想实现左侧的继承体系,它用 #define 硬写出来,局限在于它写到多少,里面就只能放多少个东西
- boost 里的名字就叫 tuple
- 概念和上面的差不多,也是硬写出来的,最多写到了针对 15 个的
tuple_base
这一页放不开,放在上一页
其他参考: