移动语义
右值引用与移动构造函数结合,可以起到无拷贝所有权转移的效果。
C++11的标准库中的std::move(),可以将一个左值强制转换为右值,相当于一个static_cast。
move函数的名字有迷惑性,实际上没有move任何东西,move的效果是移动构造/赋值函数做到的。
下面这个例子展示了move的效果,使用std::move使得调用的是移动构造函数而不是复制构造函数;
在移动构造函数里,c的地址的所有权转移了。记得在移动构造函数里把原指针清一下,要不然还有重复释放问题。
#include <iostream>
using namespace std;
class HugeMem
{
public:
HugeMem(int size): sz(size>0? size : 1)
{
c = new int[sz];
}
~HugeMem() {delete [] c;}
HugeMem(HugeMem && hm): sz(hm.sz), c(hm.c)
{
hm.c = nullptr;
}
int * c;
int sz;
};
class Moveable
{
public:
Moveable():i(new int(3)), h(1024) {}
~Moveable() {delete i;}
Moveable(Moveable && m): i(m.i), h(move(m.h))
{
m.i = nullptr;
}
int * i;
HugeMem h;
};
Moveable GetTemp()
{
Moveable tmp = Moveable();
cout << hex << "Huge Mem from " << __func__ << " @" << tmp.h.c << endl;
return tmp;
}
int main()
{
Moveable a(GetTemp());
cout << hex << "Huge Mem from " << __func__ << " @" << a.h.c << endl;
}
// g++ -std=c++11 test.cpp -o test -fno-elide-constructors
// Huge Mem from GetTemp @0xc15c40
// Huge Mem from main @0xc15c40
对于上面的例子,如果调用拷贝构造函数,会有指针重复释放问题;
如果在拷贝构造函数里做深度拷贝,又会有拷贝开销。
因此单开一个移动构造函数,去完成移动的语义。
默认的移动构造函数实际上跟默认的拷贝构造函数一样,只做一些按位拷贝的工作;
为了达成移动,必须自定义移动构造函数。
另外,前面某篇也提到了,一旦自定义拷贝构造/赋值函数、移动构造/赋值函数四者之一,另外三个都会自动被delete。
RVO/NRVO
RVO是返回值优化,也称NRVO。我们在上文程序加编译参数-fno-elide-constructors是为了disable掉RVO,要不然编译器默认会做返回值的优化,减少拷贝次数,我们显式的优化就看不到了。
但RVO只能做一部分事情,其余的例如前面的指针移动,我们显式用移动语义完成。
引用折叠与完美转发
通过引用折叠的语法可以完成完美转发。
直观地讲,1+1折叠成1,1+2或2+1折叠成1,2+2折叠成2。
通过引用折叠,无论模板参数是何种类型(左值、右值、左值引用、右值引用),都可以转移给子函数,而不发生编译报错。
template <typename T>
void IamForwording(T && t)
{
IrunCodeActually(static_cast<T &&>(t));
}
// 可以换成std::forward函数,功能和static_cast<T &&>一样,也和std::move一样
template <typename T>
void IamForwording(T && t)
{
IrunCodeActually(forward(t));
}
小结
我用了三篇介绍了移动语义相关特性。
左值、右值是C++98就有的概念,C++11中进行了强调,并扩展了右值的范围,引入了右值引用。右值引用是非常重要的类型,大大提高了传递临时量的效率,避免多次拷贝,std::move可以强制转成右值引用。
C++11在拷贝构造函数基础上,增加了以右值引用为参数的移动构造函数。移动构造/赋值只是个框架,我们可以在其中实现所有权转移的效果。
C++11新增了引用折叠的语义,基于此我们可以实现完美转发,为泛型编程增砖添瓦。