理解右值语义

右值语义是C++11引入的一项特性,本文从个人理解的角度来解释右值语义的作用,希望能对您有帮助。

要理解右值语义,首先需要知道对象与资源。一个对象可能持有一个资源,或者不持有资源。

class Object {
public:
    // 不持有资源
    Object() {
        resource = nullptr;
    }
       // 持有一个int*资源
    Object(int *ptr) {
        resource = ptr;
    }
    // 传值
    Object(const Object &obj) {
        resource = new int();
        *resource = *obj.resource;
    }
    // 析构时需要对不持有资源进行单独处理
    ~Object() {
        if (resource) {
            delete resource;
        }
    }
private:
    int *resource;
};

当你传值去构造一个对象时,按照值语义,构造出来的对象与原对象之间是相互独立的,即对构造出来的对象进行任意修改都不应影响到原对象。(想想看,在传值一个int的时候,对形参的改变会影响到实参吗?)所以,当你写复制构造函数和重载复制赋值运算符时,应该进行一次深拷贝,对应到动态内存管理中,就是自己需要再分配一块动态内存,并将原动态内存上的值相对应的复制过来。

所以我们为什么需要区分左值右值呢?因为一个左值往往代表着后面还需要使用它(它可以被后续使用到,它仍能被标识),而右值意味着使用机会只有这一次(它没有被标识,后续根本无法引用它)。在这个地方,我们就可以对右值进行一次性能优化,既然对象将要消亡,我们应该把对象持有的资源“移动”到需要它的地方。落实到动态内存管理中,我们可以进行一次浅拷贝,并直接将原对象指针置为空,这样就节省了一次内存分配与释放的开销。

Object a = Object(new int(1));
// 右边是一个临时对象,按理来说我们可以进行一次“移动”操作,然而按照值语义,我们只能对其进行一次深拷贝

解决方案是对值语义进行一次扩充,我们需要区分左值与右值,在传递左值与右值时进行两组不同的操作。通过加入右值引用,我们可以重载出移动构造函数和移动赋值运算符。对于左值,我们可以使用复制类型的函数保证独立性,对于右值,我们可以使用移动类型的函数提升其效率。在传递容器时,这显得相关重要,因为每一次深拷贝的开销都将是巨大的。

延申阅读

eg1. C++11中将值类型具体分为了左值,将亡值,纯右值。

eg2. 对于左值最后一次使用,可以使用std::move提高效率,它将一个左值转换为右值。

eg3. 通过左值引用,右值引用类型可以区分左值右值,结合C++模板推导规则,搞出了万能引用,通过将左值右值信息编码在类型信息中,实现了完美转发。

eg4. 常见的场景中,一般在函数出参入参会牵涉到右值语义的问题,详细见Effective Modern C++ 条款25

eg5. 为什么c++的queue的pop没有返回值?