Marvin's Blog【程式人生】

Ability will never catch up with the demand for it

13 Mar 2015

C++1x右值引用在标准容器中的作用

完整的例子可以从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

Categories

Tags