C++黑魔法系列2: lvalue, move constructor, copy and swap

1. 左值和右值

最最直观的例子就是:

a = 1;

a是左值,1是右值。实际上左值和右值的概念不是如此直白的。

历史

左值和右值最初是在CPL中引入的,表示“赋值之左”和“赋值之右”
在C中,lvalue指定义object的expression,全名为locator value
到了C++,lvalue加入了函数,并且规定ref能绑定到左值,但是const ref才能绑定到右值

概念

C++中expression有两个属性

  • type比如int, float
  • value category:在C++11中,表达了expression的两种独立性质:
    • 有identity:能否确定和另外一个expression指向同一实体。
      匿名对象、通过隐式函数转换的对象是没有identity的,在上个例子中,a有identity
    • 能移动:该expression能否支持移动语意
      可以参考shared_ptr的概念,如果通过赋值,可以转移资源,那么就是可移动的

通过这两个性质,可以将expression分成lvalue, xvalue, prvalue

  • lvalue:有identity,不可移动,可以多态,可以取地址
  • xvalue:有identity,可被移动,可以多态
  • prvalue:无identity,可被移动,不能多态,不能有cv限定符(const或volatile),必须有完整类型

有identity表达式的统称为glvalue(generalized lvalue泛左值)表达式
可被移动的表达式统称为rvalue表达式

lvalue

  • 任何变量&函数名
  • 返回type &的函数调用(包含a=b, ++a, *p, a[n], static_cast<int&>(x)
  • 字符串
  • 返回函数的右值引用的函数调用(static_cast<void (&&)(int)>(x)
  • a.m, p->m, 除去m为enum或非静态成员函数

prvalue

  • 除字符串以外的值(1, true)
  • 返回type的函数调用(a+b, a++, &a, static_cast<double>(x), (int)42)
  • a.m, p->m, m为enum或非静态成员函数
  • this指针
  • lambda表达式([](int x){ return x * x; }

xvalue

  • 返回type&&类型的函数调用(static_cast<char&&>(x)
  • a是右值时,a.m, a[n]

lvalue, xvalue和rvalue的关系

move constructor

C++11引入的move constructor就是为右值而设立的。

RVO & NRVO

首先我们来看看下面的代码会发生什么:

string f()
{
    string tmp("123");
    return tmp;
}
string s(f());

首先:构造tmp
第二步:在调用f()的地方,用tmp的值拷贝构造临时对象
第三步:用临时对象的值拷贝构造s
第四步:析构tmp
第五步:析构临时对象
会调用一个constructor以及两个copy constructor。

实际上,这里会使用RVO(return value optimization)优化,可以在编译阶段消除这个临时的string对象,以及拷贝构造函数的调用。
实际运行代码可能如下:

//伪代码
string after_rvo_f(string & ret)
{
    string tmp("123");
    ret.string::string(tmp); //copy ctor
    return;
}

此时,将s作为参数传入f中,消除了临时对象的创建,优化过后,只会调用一个copy constructor。

类似的,还有NRVO(named return value optimization),它也是RVO的变种,它可以进一步消除。例如前面的函数f会被优化,优化后运行代码如下:

// 伪代码 after nrvo
string after_nrvo_f(string & ret)
{
    ret.string::string("123"); //ctor
    return;
}

在vs release下运行时,只有一个constructor调用;
但是在vs里debug模式下运行时,并不会开启NRVO
此时,由于g()的返回值是一个prvalue,会优先使用move constructor,而不是copy constructor。结合RVO,实际代码如下:

//伪代码
string actural_debug_g(string & ret)
{
    tmp.string::string("123");          // ctor
    ret.string::string(string && tmp);  // move ctor
    return;
}

copy elision

在C++17中,强制规定了copy elision。可以强制编译器忽略copy constructor和move constructor。
copy elision包括:

  • RVO
string fun()
{
    string s = string("123");   // string("123")是prvalue
                                // 初始化s时不会调用copy assignment,只会初始化string一次
    return s;                   // 从s到临时结果对象会用到NRVO(非copy elision)
                                // 如果没有,则调用move constructor
}
string res = fun();             // fun()返回的临时结果对象是prvalue
                                // 初始化res时不会调用copy assignment
  • 对value类型的参数,用prvalue传入时,不会调用copy constructor
void h(string s) {...};
h(string());            // string()返回的是prvalue,
                        // 初始化h时不会调用copy constructor,只会初始化string一次
  • 按value捕获的异常

move constructor

move的开销比RVO大,比copy小。RVO不仅会节省一个copy constructor,也会省去一个临时变量的destructor的开销。
当用右值初始化对象时,会调用move constructor(如果定义了),包含:
1. 初始化:T a = std::move(b); T a(std::move(b));
2. 函数参数传递:f(std::move(b)); 有void f(T)
3. 函数返回:T f() 中的 return a;(在没有开启NRVO时)

std::move的本质是一个强制类型转换,将T转为T&&

move constructor的参数是rvalue,它会做以下几件事:
1. 把old object的资源“偷”到new object里
2. 把old object的资源指针重置

string(string && str)
{
    data = str.data;
    str.data = NULL;
}

记住:不要显示的使用std::move,因为这样编译器就没法使用RVO了。如果允许RVO,return by value!

string bad_move1()
{
    string tmp("123");
    return std::move(tmp);   //violate copy elision
}

string&& bad_move2()
{
    string tmp("123");
    return std::move(tmp);   //return a reference of destruted tmp.
}

rule of 3

任何实现了destructor, copy constructor, copy assignment其中之一的类可能需要实现另外的两个。
这常常出现于资源的管理类,这种类需要处理资源释放、深拷贝等问题。
如果使用系统默认的copy constructor和copy assignment,memberwise的拷贝可能会多次释放同一资源;
类似的,如果不实现destructor,可能会造成内存泄漏。

rule of 5(实际上是4)

在引入了move constructor以后,rule of 3可以扩展为rule of 5:
任何实现了destructor, copy constructor, copy assignment, move constructor, move assignment其中之一的类可能需要实现另外的四个。
move constructor, move assignment的作用主要是优化深拷贝带来的性能开销。
这五个的函数签名如下:

  • copy constructor: A(const A& other)
  • copy assignment: A& operator=(const A& other)
  • move constructor: A(A && other)
  • move assignment: A& operator=(A&& other)
  • destructor: ~A()

rule of 0

我个人的理解是,如果一个类不需要管理资源,那么它就不需要实现destructor, copy constructor, copy assignment, move constructor, move assignment中的任何一个。
C++11规定,如果定义了move constructor,那么必须自己定义copy constructor。这么做有一部分原因是为了向前兼容,从C++11才有的move constructor,这样不需要修改太多代码。
同时,C++11规定,如果定义了destructor,那么默认的move constructor是deprecated的,在未来某个版本可能会删掉。

copy and swap idiom

最后一个议题是如何写copy constructor, copy assignment, move constructor, move assignment。

copy constructor

copy constructor需要注意点有:

  • 深拷贝
  • 空间不够抛异常

copy assignment

需要注意的点有:

  • 删除旧值
  • 自赋值
  • 深拷贝
  • 空间不够异常

copy and swap会做以下事情:
1. 利用copy constructor,创建一个other副本作为参数传入
2. 然后swap this和副本other的内容,此时,this存放了other的深拷贝,other存放了this的旧值
3. 函数返回,此时存放旧值的other被析构

A& operator=(A other)
{
    swap(other);
    return *this;
}

swap只是简单的将成员的ownership转换,不需要创建或者销毁任何东西。

深拷贝的问题在传入参数的地方由copy constructor搞定;
空间不够异常的情况下,发生在other副本创建的时候,不会对=右边的对象产生影响;
自赋值的情况下,会多一个拷贝构造一个析构的开销,除此以外不会有其他问题。

move constructor

move constructor只需要调用swap函数即可

A(A&& other)
{
    swap(other);
}

move assignment

有了copy assignment以后,我们不需要再定义move assignment了。为什么?
考虑以下语句
A a1 = a2
如果a2是个lvalue,则会调用copy constructor创建副本other。这时,other无论如何修改,都和a2无关
如果a2是个rvalue,C++11则会调用move constructor创建副本other。这时,a2的内容已被“移动”至other中,接下来swap时,这些内容又会进一步被“移动”至a1里。

recall rule of 5, now it’s rule of 4


参考资料
1. http://en.cppreference.com/w/cpp/language/value_categoryx
2. http://www.cnblogs.com/kex1n/archive/2010/05/26/2286488.html
3. https://msdn.microsoft.com/zh-cn/library/ms364057(v=vs.80).aspx
4. https://www.ibm.com/developerworks/community/blogs/5894415f-be62-4bc0-81c5-3956e82276f3/entry/RVO_V_S_std_move?lang=en
5. https://stackoverflow.com/questions/4986673/c11-rvalues-and-move-semantics-confusion-return-statement?lq=1
6. https://msdn.microsoft.com/zh-cn/library/ms364057(v=vs.80).aspx
7. http://en.cppreference.com/w/cpp/language/move_constructor
8. https://stackoverflow.com/questions/12953127/what-are-copy-elision-and-return-value-optimization/12953145#12953145
9. http://en.cppreference.com/w/cpp/language/copy_elision
10. http://en.cppreference.com/w/cpp/language/rule_of_three
11. https://stackoverflow.com/questions/11255027/why-user-defined-move-constructor-disables-the-implicit-copy-constructor
12. https://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom
13. https://stackoverflow.com/questions/3106110/what-are-move-semantics

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值