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

杂货边角(17):C++11的右值引用和移动语义

2018-02-12 18:33 465 查看
相信绝大多数人,在写C++代码时都会被函数返回值的多次传递弄混淆过。事实上,这涉及到堆栈的运行,对于一个子函数而言,除非程序员显式地调用new操作符,否则函数创建的对象只能在自己的临时栈上,子函数和父函数的临时栈不同,从而这必然存在多次传递的情况。以前在C++标准没有涉及到该问题时,都是编译器自作主张地进行优化,即所谓的RVO(return value optimization),但是编译器提供的RVO也只能在某系局限的场景下发挥作用,可适用性不足。所以还是得靠C++标准来提供相关方面的支持:这便是右值引用和移动语义。

#include <iostream>
#include <type_traits>

using namespace std;

class HasPtrMem {
public:
HasPtrMem() : d(new int(3)) {
cout << "Construct: " << ++n_cstr << endl;
}

HasPtrMem(const HasPtrMem & h) : d(new int(*h.d)) {  //拷贝构造函数
cout << "Copy construct: " << ++n_cptr << endl;
}

HasPtrMem(HasPtrMem && h) : d(h.d){   //移动构造函数 ,但在现今的编译器中,其实一般都由编译器默认实现
h.d = nullptr;                    //将传入的临时对象的指针成员置为空,从而实现鸠占鹊巢
cout << " Move construct:" << ++n_mvtr << endl;
}

~HasPtrMem() {
delete d;
cout << "Destruct: " << ++n_dstr << endl;
}

int * d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};

int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_mvtr = 0;
int HasPtrMem::n_cptr = 0;

HasPtrMem GetTemp() {
HasPtrMem h;
cout << "Resource from " << __func__ << ":" << hex << h.d << endl;  // ‘<<hex’代表后续数值输出将按照16进制输出
return h;
}

//HasPtrMem GetTemp() { return HasPtrMem(); }

int ReturnRvalue()
{
int temp = 4;
return temp;  //函数返回的临时对象,属于右值
}

struct Copyable {
Copyable() { }
Copyable(const Copyable &o ) {
cout << "Copied" << endl;
}
};

Copyable ReturnRvalue1() { return Copyable(); }
void AcceptVal(Copyable) { cout << "inside the AccepetVal" << endl; }
void AcceptRef(const Copyable &) { cout << "inside the AcceptRef" << endl; }

/*===================================================================================
**主角:       std::move
**=================================================================================*/
class HugeMem {
public:
HugeMem(int size) : sz(size > 0 ? size : 1){ c = new int[sz];}
HugeMem(HugeMem && hm) : sz(hm.sz), c(hm.c) {hm.c = nullptr;cout << "wola" << endl;}
//HugeMem(const HugeMem& hm) : sz(hm.sz),c(new int[hm.sz]) { cout << "hola" << endl;}
~HugeMem() { delete [] c; }
int * c;
int sz;
};

class Moveable {
public:
Moveable():i(new int(3)), h(1024) {}
Moveable(Moveable && m): i(m.i),h(move(m.h)) {m.i = nullptr; cout << "fux" << endl;} //启用h(move(m.h)),显式地调用HugeMEM对象的移动构造函数,否则这里m.h将当作左值,调用的是拷贝构造函数
~Moveable() {delete i; }

int* i;
HugeMem h;
};

Moveable GetTemp1() {
Moveable tmp = Moveable();
cout << hex << "Huge Mem from " << __func__ << "@" << tmp.h.c << endl; //Huge Mem from GetTemp xxx
return tmp; //tmp作为返回值,将成为右值
}

int main()
{
//HasPtrMem a = GetTemp();
//cout << "Resource from " << __func__ << ":" << hex << a.d << endl;
int a;
//int && b1 = a;  //cannot bind 'int' lvalue to 'int &&'

//int & b2 = ReturnRvalue(); //invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
const int & b3 = ReturnRvalue();

cout << " Pass by value: " << endl;
AcceptVal(ReturnRvalue1());  //这里存在RVO优化,需要手动关闭编译器的优化,才能看见一次形参的拷贝构造
cout << " Pass by reference: " << endl;
AcceptRef(ReturnRvalue1());

/*===================================================================================
**<type_traits>  使用3个模板类:is_rvalue_reference\ is_lvalue_reference\ is_reference
**配合decltype等类型推导手段,可以快速判断一个变量是左值引用还是右值引用
**使用is_move_constructible\ is_trivially_move_constructible\ is_nothrow_move_constructible
**可以判断一个类型是否可以存在移动语义
**=================================================================================*/
cout << is_rvalue_reference<string &&>::value << endl;  //1
string && a1 = "helloworld";
cout << is_rvalue_reference<decltype(a1)>::value << endl; //1
cout << is_move_constructible<int>::value << endl;  //1

/*===================================================================================
**主角:       std::move
**功能: 将一个左值强制转化为右值引用,继而可以通过右值引用使用该值
等同于static_cast<T&&>(lvalue)
**库:   <utility>
**配合decltype等类型推导手段,可以快速判断一个变量是左值引用还是右值引用
**=================================================================================*/
Moveable a2(GetTemp1());
cout << hex << "Huge Mem from " << __func__ << " @ " << a2.h.c << endl;

HugeMem a4(100);
//HugeMem a5(a4);
}


其实将右值引用和左值引用放在一起并可以很好的理解:左值是存在名称,可以取地址的变量等,如全局变量、函数符号等,右值是不存在名称,无法直接取地址的对象,如非引用类型的函数返回值、true、1+2这类对象,C++98此前规定对变量的取引用便是左值引用,而C++11引入的右值引用便是针对右值重复构造存在资源浪费情况设计的,右值引用可以为移动语义(
void func(T&&)
即鸠占鹊巢,移动构造等)创造匹配的参数。

这其中存在左值引用
T&
,常量左值引用
const T&
,右值引用
T&&
,常量右值引用
const T&&
(仅仅只是出于语义的对称性,其实功能和常量左值引用冲突,不建议使用)。

其中左值引用和右值运用都必须定义时即初始化,即
T & ref = new T
,归根结底,因为‘引用’的语义就是需要绑附在具体的对象上,其中左值引用只能使用左值初始化,右值引用也只能使用右值初始化,即

T& lvalueRef = (T)value; //pass
T& lvalueRef = ReturnRvalue(); //error
T& lvalueRef = (const T)value; //error

T&& rvalueRef = (T)value; //error
T&& rvalueRef = ReturnRvalue(); //pass


而常量左值引用,则是个奇葩,可以使用任何值来初始化,考虑到常量左值引用的语义限制级别最高,所以也是可以理解为什么常量左值可以做到来者不拒。

const T& conLRef = value; //pass 非常量左值
const T& conLRef = const value; //pass, 常量左值
const T& conLRef = (const)ReturnRvalue(); //pass,常量右值
const T& conLRef = ReturnRvalue(); //pass,非常量右值


这也是为什么类中的拷贝构造函数
type(const type&) {}
可以为移动构造函数
type(const type&&) {}
兜底的原因。编译器会优先去匹配右值引用类型的构造函数,如果类没有定义,则可以将该次初始化丢给拷贝构造函数来兜底,因为拷贝构造函数的形参是常量左值引用类型,可以接受任何参数。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息