分类: C/C++
2013-02-04 12:25:10
这个绝对是新增的top特性,篇幅非常多。看着就有点费劲,总结更费劲。
原来的标准当中,参数与返回值的传值形式涉及到对象的复制,传值完成后,中间产生的临时对象又会马上被销毁,某些自定义的对象或者容器有很多元素时复制的开销非常大,而且例如IO对象或unique_ptr对象也不允许复制;传址在返回值的某些场景又会有局部对象的问题。c++11中新增了move的概念,用一个词描述就是"steal"。
1. 右值引用相关
个人理解,能出现在赋值运算符左侧即能被赋值的对象称之为左值,类似地右侧的赋值对象称为右值,都可以是单一变量或表达式。左值有其自己的identity,程序相对长期地为其永久或暂时地分配内存空间;右值多为临时变量(如某些表达式的中间值),生命期相对短暂(ephemeral),所有的字面常量均为右值。一个整形变量i,其为左值,我们可以赋给i任何其能存储的整型值;i+5这个表达式是右值,因为无论i是多少,你都不可能把20赋值给(i+5)。
右值引用必须绑定在右值上,书中的原话绑定的值是 an object that is about to be destroyed. 所以不能绑在左值上。
普通(regular)左值引用也不能绑定在右值上,const引用可以绑常量。
以前标准中的引用都是左值引用,运算符“&”,右值引用的运算符是"&&"。
int i = 42; int &r = i; // ok: r refers to i int &&rr = i; // error: cannot bind an rvalue reference to an lvalue int &r2 = i * 42; // error: i * 42 is an rvalue const int &r3 = i * 42; // ok: we can bind a reference to const to an rvalue int &&rr2 = i * 42; // ok: bind rr2 to the result of the multiplication上面是几个书中的例子。注意:rr2是个右值引用,是说它绑定的是个右值,但其本身是左值,即下面这语句非法:
int &&rr3 = rr2; // error: the expression rr2 is an lvalue!即凡是可以 var_type var_name; 这样定义出来的变量(variable)其自身都是左值。
2. std::move相关。
右值引用因为绑定对象即将被销毁,意味着没有人会继续访问他们,所以就可以把他们(的资源)steal过来。
虽然不能将右值引用绑在左值上,但通过利用utility头文件新增的函数模板move,它返回传入对象的右值引用,可以达到 steal的效果。
int &&rr3 = std::move(rr2); // ok再提醒:一旦使用了move,编译器就默认传入对象已经不打算使用了,是可以被销毁的,move之后该对象的值已经不确定,不要再访问。还有由于对象偷取与复制的差别巨大,不注意会产生非常难定位的bug,所以所有使用move的地方一定要使用全称std::move,给大家以提醒。(其实c++11在algorithm头文件也新增了一个move,参数与意义都与此截然不同)。
3. move构造函数
对象可以被复制构造,满足条件场景下,自然也就可以被“剪切”构造。move构造函数就用于此,它也属于copy-control系列函数,后者的5个成员现在更新为:拷贝构造函数,复制性质的赋值运算符,move构造函数,move性质的赋值运算符,析构函数。
class base { public: int* p; base():{p = new(...);} ~base(){delete p;} base(base&& ref):p(ref.p){ref.p = nullptr;} };新创建对象的指针p接管了来源对象指针p所持有的资源,并将后者置空。特别注意move构造函数一定要来源对象内部存储资源的变量设置正确的状态,如指针置空等,避免来源对象析构时内存误释放。
对应的赋值运算符则一定要判断比较this指针和来源对象地址是否相同,进行自我move操作的保护。
move构造函数创建新对象后,来源对象并不是马上被销毁,但是出于一种析构函数随时可以运行的状态,不要再去访问它的值,这已经不可预计。
4. move构造函数的合成关系(喂,SBL你的inside c++11 object model啥时出版啊)
考构和其赋值运算符如果我们没有声明自己的,编译器会默认合成出来一个memberwise copy版本的。move构造函数和运算符却不是如此。如果类中有声明自己的考构和赋值运算符,或析构函数,则编译器不会合成默认的move操作函数,这时如果进行右值引用的move操作调用,实际触发的将会是拷贝构造函数。
当类中没有声明自己的任何一个copy-control函数,并且当所有非静态成员都可以move时,才会合成默认的move构造函数。
move构造函数和运算符从不会(never)隐式(implicit)被认为是delete,当我们声明其为default时,下列场景会变delete
(1)当类有某成员定义了其考构而没有m-con时,此类的m-con编译器认为是delete的,mv运算符同理,递归定义就是子成员的m-con合成不出来,该类的也肯定合成不出来。
(2)子成员的m-con或运算符是私有或delete,则此类的m-con和运算符是delete。
(3)类似考构,子成员或本身的析构是私有或delete,m-con为delete。
(4)类似赋值运算符,有const成员或引用成员,move运算符为delete。
经过试验,以下个人理解:在没有move成员时,都会调用考构成员,毕竟move风险太大。move成员的default最好不要随便加,没加时,调用std::move编译器还可以换成考构,加了后表示就要move,一旦有上面限制无法合成就会报错,虽然是编译的,不熟悉的搞起来还是有点麻烦。可能是类似提示,不知道为什么扯上了 constexpr
error: defaulted declaration 'base::base(const base&&)'
error: does not match expected signature 'constexpr base::base(base&&)'
BT的来了:move成员会反作用于考构成员,有定义move系列,没定义考构系列时,考构系列被认为是delete的。在base内定义实现了move系列成员,没有考构成员,下面的obj2两种形式定义都不会成功。
base obj1; base obj2 (obj1); base obj2= obj1;提示都一样,左值不能绑到右值引用上,默认直接进入了move构造函数:
error: cannot bind 'base' lvalue to 'base&&'
error: initializing argument 1 of 'base::base(base&&)'
结论:
(1)定义了move系列,则一定要定义考构系列。
(2)考构和move都有,左值引用进考构,右值引用进move.
(3)有考构没move,都进考构,即使通过std::move.
(4)copy-control系列的5个成员,自定义了一个,其余的最好都定义。
base obj2,obj1; obj2 = base(5);//obj2 = std::move(base(5)); obj2 = obj1;考构,mv都有,2行mv,3行考构运算符;
只有考构,都进考构运算符,无论第二行加不加std::move。
5. move迭代器,make_move_iterator,在iterator头文件,不导入也能用,把普通迭代器转成move迭代器。
vectorivec{1,2,3}; vector ivec2(ivec.begin(),ivec.end()); vector ivec3(make_move_iterator(ivec.begin()), make_move_iterator(ivec.end())); cout< 输出:3 3 3 3 4 。ivec2复制ivec的内容,ivec3则是“偷”了过来,虽然ivec的size仍是3,但内部已经不能保证,在ivec3压入元素0后,其size为4,但ivec的size仍为3。