右值引用、move与move constructor

2023-05-24,,

http://blog.chinaunix.net/uid-20726254-id-3486721.htm

这个绝对是新增的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引用可以绑常量。

以前标准中的引用都是左值引用,运算符“&”,右值引用的运算符是"&&"。

 

1
2
3
4
5
6

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是个右值引用,是说它绑定的是个右值,但其本身是左值,即下面这语句非法:

 

1

int &&rr3 = rr2;   // error: the expression rr2 is an lvalue!

即凡是可以 var_type var_name; 这样定义出来的变量(variable)其自身都是左值。

2. std::move相关。

右值引用因为绑定对象即将被销毁,意味着没有人会继续访问他们,所以就可以把他们(的资源)steal过来。

虽然不能将右值引用绑在左值上,但通过利用utility头文件新增的函数模板move,它返回传入对象的右值引用,可以达到 steal的效果。

 

1

int &&rr3 = std::move(rr2);   // ok

再提醒:一旦使用了move,编译器就默认传入对象已经不打算使用了,是可以被销毁的,move之后该对象的值已经不确定,不要再访问。还有由于对象偷取与复制的差别巨大,不注意会产生非常难定位的bug,所以所有使用move的地方一定要使用全称std::move,给大家以提醒。(其实c++11在algorithm头文件也新增了一个move,参数与意义都与此截然不同)。

3. move构造函数

对象可以被复制构造,满足条件场景下,自然也就可以被“剪切”构造。move构造函数就用于此,它也属于copy-control系列函数,后者的5个成员现在更新为:拷贝构造函数,复制性质的赋值运算符,move构造函数,move性质的赋值运算符,析构函数。

 

1
2
3
4
5
6
7
8
9
10

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两种形式定义都不会成功。

 

1
2
3

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个成员,自定义了一个,其余的最好都定义。

 

1
2
3

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迭代器。

 

1
2
3
4
5
6
7
8
9

    vector<int> ivec{1,2,3};
    vector<int> ivec2(ivec.begin(),ivec.end());
    vector<int> ivec3(make_move_iterator(ivec.begin()), make_move_iterator(ivec.end()));
 
    cout<<ivec.size()<<endl; cout<<ivec2.size()<<endl;="" cout<<ivec3.size()<<endl;="" ivec3.push_back(0);="" cout<<ivec.size()<<endl;="" cout<<ivec3.size()<<endl;<="" pre="">
输出:3 3 3 3 4 。ivec2复制ivec的内容,ivec3则是“偷”了过来,虽然ivec的size仍是3,但内部已经不能保证,在ivec3压入元素0后,其size为4,但ivec的size仍为3。
<p>
    <br>
</p>                                   </ivec.size()<<endl;></int></int></int>

 

l

右值引用、move与move constructor的相关教程结束。

《右值引用、move与move constructor.doc》

下载本文的Word格式文档,以方便收藏与打印。