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

相信绝大多数人,在写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&&) {}兜底的原因。编译器会优先去匹配右值引用类型的构造函数,如果类没有定义,则可以将该次初始化丢给拷贝构造函数来兜底,因为拷贝构造函数的形参是常量左值引用类型,可以接受任何参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值