您的位置:首页 > 编程语言 > C语言/C++

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

2017-08-15 20:43 501 查看

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
,
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



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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: