完整的例子可以从gist取得。
C++11引入了右值引用这个特性,上一篇文章介绍了什么是右值引用,但是什么情况下使用右值引用能够改善程序的性能呢?
C++中的标准容器库是很有用的工具,我们可以在容器中存放像char, int之类的标量(scalar type),也可以存放自定义的类对象。这就要求类对象在语义上尽可能得与标量表现一致,如以下例子:
假设我们定义一个类LitInt
(取literal int之意),LitInt
支持字符串形式的数字,并且在使用上LitInt
要与int
一致:
要使上面的例子能够成立,LitInt
必须有下列构造函数和析构函数定义:
下面是LitInt
的实现以及main
函数:
使用GCC 4.9.2编译:g++ -std=c++11 LitInt.cpp
,程序的运行结果是:
可以看到,执行LitInt a("1,000,000,000,000,000");
时有一次构造函数调用;执行LitInt b = a;
时有一次拷贝构造函数调用;程序结束时有两次析构函数调用。 那么,假设我们再定义一个x,并用一个右值去初始化x会发生什么情况呢?
在C++中,右值通常是临时的,出现在等号右边的,没有名字的值。
编译出错,结果显示:
出现以上错误的原因在于拷贝构造函数LitInt(LitInt& other)
的参数类型是LitInt&
,但是只有const
引用才能绑定右值,所以我们需要将拷贝构造函数改成LitInt(const LitInt& other)
。
可以看到LitInt x = LitInt("9,999,999,999,999,999")
只触发了一次构造函数调用,这里编译器作了Copy Elision的优化,避免了一次构造函数的调用。 现在我们想把LitInt
放到std::vector
里面去:
结果显示:
在上面的例子中我们用vli.reserve(2)
;来在vli
里面预留俩个LitInt
的空间,如果这时候再把LitInt x
进行push_back
会有什么结果呢?
vli
势必要重新分配更多的空间,并将原来空间的LitInt a,b
拷贝到新的空间中去。结果如下所示:
在std::vector调整空间大小的时候移动了存在里面的LitInt,对于每个被移动的LitInt,都要在新的空间重新构造,这需要三步操作:
- 重新申请value
- 把旧的value拷贝过去
- 释放旧的value
明眼人一看就知道问题所在了:既然value
是一个指针,为何不直接把指针的值传递过去?为了解决这个问题C++11引入了移动语义(move semantics)。为了让LitInt
支持移动语义,需要添加一个移动构造函数(move constructor):
LitInt(LitInt&& other)
就是移动构造函数,它直接传递value
指针的值,而不分配和释放内存;noexcept
关键字说明当前函数不会抛出异常,这样std::vector
才能放心调用该移动构造函数。
看看现在的运行结果:
可以看到,在vli.push_back(x)
的时候少了两次内存的申请和释放操作。
更深入阅读:
- Universal References in C++11
- C++11 Rvalue References Explained
- C++11 Emplace VS Insert