C++11 左值、右值

在C++11之前,已经有了左值和右值的概念,但程序员可以无视左值和右值的概念,这并不影响编程的逻辑,但C++11之后,正确使用右值可以使代码更可读,更高效。

什么是左值

直观上在等式左边的是左值,在等式右边的是右值。

	a = b + c

a在等式的左边为左值,b + c在等式右边为右值。实际上C++对左值、右值有更加规范的定义,能取地址的叫左值,如可以对a取地址&a,不能取地址的叫右值,对b+c取地址,&(b + c)是非法的。

浅拷贝与深拷贝

C++的对象经常会包括一些指针成员变量,因为c++会默认生成拷贝构造函数,且默认使用浅拷贝,如果不能正确处理指针成员变量,会因为double free而导致core掉。

//code 1
#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {}
    ~HasPtrMem() { delete d; }
    int * d;
};

int main() {
    HasPtrMem a;
    HasPtrMem b(a);
    cout << *a.d << endl;   // 0
    cout << *b.d << endl;   // 0
}   // 析构:运行时错误,多次在同一位置调用delete

正确的做法是,自己正确实现拷贝构造函数,使用深拷贝的方式:

//code 2
#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {}
    HasPtrMem(const HasPtrMem & h): 
        d(new int(*h.d)) {} // 拷贝构造函数,从堆中分配内存,并用*h.d初始化 
    ~HasPtrMem() { delete d; }
    int * d;
};

int main() {
    HasPtrMem a;
    HasPtrMem b(a);
    cout << *a.d << endl;   // 0
    cout << *b.d << endl;   // 0
}   // 正常析构析构

深拷贝带来的问题

看下面一段代码:

//code 3
#include <iostream>
using namespace std;

class HasPtrMem {
public:
    HasPtrMem(): d(new int(0)) {
        cout << "Construct: " << ++n_cstr << endl; 
    }
    HasPtrMem(const HasPtrMem & h): d(new int(*h.d)) {
        cout << "Copy construct: " << ++n_cptr << endl;
    }
    ~HasPtrMem() { 
        delete d;
        cout << "Destruct: " << ++n_dstr << endl;
    }
    int * d;
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
};

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

HasPtrMem GetTemp() { return HasPtrMem(); }

int main() {
    HasPtrMem a = GetTemp();
}
//compile g++ code3.cpp -fno-elide-constructors

这段代码的逻辑比较简单,只做了一件事就是从GetTemp中生成一个HasPtrMem对象并赋给a,代码的输出结果是:

Construct: 1
Copy construct: 1
Destruct: 1
Copy construct: 2
Destruct: 2
Destruct: 3

可以看到只做了这么简单的一件事,却调用3次构造函数,3次析构函数。第1次构造函数发生在函数体GetTemp内部,第2次发生在从函数体内部的对象拷贝构造到函数体外部的临时变量,第3次由临时变量拷贝构造到a变量,例子中仅仅是一个int的指针,如果是很大的内存,或其他复制起来比较消耗资源的成员变量,代码将变得非常低效。

因此在之前C++编程中,为了避免这种低效的代码,通常会把指针作为函数的参数传入:

void GetTemp(HasPtrMem* & ptr) { ptr = new HasPtrMem(); }

int main() {
    HasPtrMem* a; 
    GetTemp(a);
    delete a; 
}

这样的写法效率很高,只有一次构造一次析构,但代码复杂度提升了,可读性变差了,考虑下面这个例子:

func(GetObj1(), GetObj2(), GetObj3());

对比:

Obj1* p1;
GetObj1(p1);

Obj2* p2;
GetObj2(p2);

Obj3* p3;
GetObj3(p3);

func(p1, p2, p3);

上面两段代码一对比代码可读性、复杂度高下立判。在之前版本的C++中往往为了效率不得不把代码写的这么丑,但C++11提供了一种融合两种写法有点的方法。

##右值引用和移动构造函数

从code3中,多出的赋值构造函数是因为产生了临时变量,临时变量的生命周期非常短,通过赋值拷贝后生命周期就结束了,注意到这里的临时变量符合之前提到的右值定义,c++11中可以通过右值引用来给临时变量“续命”:

T && t = ReturnValue();

同理c++11利用右值引用的技术,支持对象的移动构造函数,可以完美解决code3的临时变量开销大的问题:

//code 4
#include <iostream>
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_cptr = 0;
int HasPtrMem::n_mvtr = 0;

HasPtrMem GetTemp() { 
    HasPtrMem h;
    cout << "Resource from " <<  __func__ << ": " << hex << h.d << endl;
    return h;
}

int main() {
    HasPtrMem a = GetTemp();
    cout << "Resource from " <<  __func__ << ": " << hex << a.d << endl;
}
//compile g++ code4.cpp -fno-elide-constructors -std=c++11

注意这里的移动构造函数通过右值引用的技术避免了临时变量的开销,同时通过在移动构造函数里把右值的指针成员变量赋给新的对象保证了安全性,完美解决了前面提到的效率和可读性不可兼得的问题。

理解了右值的概念,再去理解c++11里面新的std::move()和std::forward()就比较容易了。

~~~~~~

-fno-elide-constructors 注意到文中几处代码编译用到了这个参数,这个参数表示关闭编译器返回值优化,如果不适用这个参数,可以注意到编译器会非常聪明的帮我们进行了返回值优化,其中的拷贝构造和移动构造都不再需要了,因为文中代码很简单,编译器可以帮助优化掉,但编译器并不能总是做到这一点,而移动构造的方法可以解决这些编译器无法优化的问题,因此总是有用的。

Ref:《深入理解C++11》

联系我: