一、结论

  1. C 中将数据类型分为左值和右值,在 C++11 中又将右值概念更为细致的分为将亡值(xvalue)和纯右值(prvalue)
  2. 将亡值是 C++11 新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用 T&& 的函数返回值、std::move 的返回值。
  3. 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
  4. 右值引用可以直接指向右值,也可以通过 std::move 指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
  5. 作为函数形参时,右值引用更灵活。虽然 const 左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
  6. 可移动对象在需要拷贝且被拷贝者之后不再被需要的场景,建议使用 std::move 触发移动语义,提升性能。
  7. 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和 std::move 的语言特性。
  8. std::move 本身只做类型转换,对性能无影响。
  9. std::forward 同样也是做类型转换且更强大,move 只能转出来右值,forward 既可以转成右值,又可以转成左值。
  10. 函数最好不要返回函数体内局部变量的左值引用或右值引用。

二、注意事项

  • 左值引用是具名变量值的别名
  • 右值引用是不具名(匿名)变量的别名
  1. 引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值,但是,const左值引用是可以指向右值的

    const int &ref_a = 5;
    int a = 5;
    int &ref_a = a; // 左值引用指向左值,编译通过
    int &ref_a = 5; // 左值引用指向了右值,会编译失败
    
  2. 右值引用的标志是 &&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值

    int&& ref_a_right = 5; // ok
    int a = 5;
    int&& ref_a_left = a; // 编译不过,右值引用不可以指向左值
    
    ref_a_right = 6; // 右值引用的用途:可以修改右值
    
  3. 事实上 std::move 移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。 所以,单纯的 std::move(xxx) 不会有性能提升,从这个角度来讲,右值引用和左值引用的功能相似,都是原始变量的别名,至于移动后原始变量是否可用,取决于被移动的数据类型对于移动语义的具体实现。

    案例一:

    int a = 5; // a是个左值
    int &ref_a_left = a; // 左值引用指向左值
    int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向
    cout << a << endl; // 打印结果:5
    ref_a_right = 10; 
    cout << a << endl; // 打印结果:10
    ref_a_left = 20;
    cout << a << endl; // 打印结果:20
    // 这三个变量的地址都相同
    

    案例二:

    class Person{
    public:
        int m_age{20};
    };
    
    void test(Person&& p){
        cout << __func__ << " " << &p << endl;
    }
    void test1(Person& p){
        cout << __func__ << " " << &p << endl;
    }
    
    int main()
    {
        Person pp;
        cout << __func__ << " " << &pp << endl;
        test(move(pp));
        cout << pp.m_age << endl;
        test1(pp);
        return 0;
    }
    
    输出:
    main 0x7ffc06f34444
    test 0x7ffc06f34444
    20
    test1 0x7ffc06f34444
    
  4. 被声明出来的左、右值引用都是左值。因为被声明出的左右值引用是有地址的,也位于等号左边。

    // 形参是个右值引用
    void change(int&& right_value) {
        right_value = 8;
    }
    
    int main() {
        int a = 5; // a是个左值
        int &ref_a_left = a; // ref_a_left是个左值引用
        int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
    
        cout << &a << ' ';
        cout << &ref_a_left << ' ';
        cout << &ref_a_right;
        // 打印这三个左值的地址,都是一样的
    
        // error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
        // change(a); // 编译不过,a是左值,change参数要求右值
        // change(ref_a_left); //  编译不过,左值引用ref_a_left本身也是个左值
        // change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
    
        change(std::move(a)); // 编译通过
        change(std::move(ref_a_right)); // 编译通过
        change(std::move(ref_a_left)); // 编译通过
    
        change(5); // 当然可以直接接右值,编译通过
    }
    
  5. 右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。

三、std::forward

std::forward<T>(u) 有两个参数:Tu

  • a. 当 T 显示为左值引用类型时,u 将被转换为 T 类型的左值;
  • b. 否则 u 将被转换为 T 类型右值
void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    
    // error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
    // change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int&&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
    change2(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
    
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int&>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

四、自定义类支持移动语义

编译器会默认在用户自定义的 classstruct 中生成移动语义函数,但前提是符合用户类默认的移动构造/移动赋值的启用条件系统默认的移动语义函数更像是一种引用(原始变量的别名)

if (定义了 拷贝构造函数 、 拷贝赋值运算符 或 析构函数) {
    不会合成移动构造函数和移动赋值运算符。此时会使用对应 拷贝 操作来代替 移动
}
else if (类的所有成员都可以移动) {
    则会为类合成移动构造函数或移动赋值运算符。
}
else {
    合成拷贝构造函数和拷贝复制运算符。
}

C++11 对于特殊成员函数处理的规则如下:

  • 默认构造函数:和 C++98 规则相同。仅当类不存在用户声明的构造函数时才自动生成。
  • 析构函数:基本上和 C++98 相同;稍微不同的是现在析构默认 noexcept。和 C++98 一样,仅当基类析构为虚函数时该类析构才为虚函数。
  • 拷贝构造函数:和 C++98 运行时行为一样:逐成员拷贝 non-static 数据。仅当类没有用户定义的拷贝构造时才生成。如果类声明了移动操作它就是 delete。当用户声明了拷贝赋值或者析构,该函数自动生成已被废弃。
  • 拷贝赋值运算符:和 C++98 运行时行为一样:逐成员拷贝赋值 non-static 数据。仅当类没有用户定义的拷贝赋值时才生成。如果类声明了移动操作它就是 delete。当用户声明了拷贝构造或者析构,该函数自动生成已被废弃。
  • 移动构造函数和移动赋值运算符:都对非 static 数据执行逐成员移动。仅当类没有用户定义的拷贝操作,移动操作或析构时才自动生成。
class Person{
public:
    Person():m_age(new int(20)){
        cout << "Construct: " << ++n_cstr << endl;
    }
    ~Person(){
        delete m_age;
        cout << "Destruct: " << ++n_dstr << endl;
    }
    Person(const Person& p):m_age(new int(*p.m_age)){
        cout << "Copy construct: " << ++n_cptr << endl;
    }
    Person(Person&& p){
        m_age = std::exchange(p.m_age, nullptr);
        cout << "Move construct: " << ++n_mvtr << endl;
    }
    Person& operator=(const Person& p){
        *m_age = *p.m_age;
        cout << "operator=(const Person& p)..." << endl;
        return *this;
    }
    Person& operator=(Person&& p){
        if(m_age) delete m_age;
        m_age = std::exchange(p.m_age, nullptr);
        cout << "operator=(Person&& p)..." << endl;
        return *this;
    }

    int* m_age{nullptr};
    static int n_cstr;
    static int n_dstr;
    static int n_cptr;
    static int n_mvtr;
};
int Person::n_cstr = 0;
int Person::n_dstr = 0;
int Person::n_cptr = 0;
int Person::n_mvtr = 0;

Person GetPerson(){
    Person p;
    cout << *p.m_age << endl;
    cout << "Resource from " << __func__ << ": " << p.m_age << endl;
    return p;
}

int main(int argc, const char* argv[]){
    Person p = GetPerson();
    cout << *p.m_age << endl;
    cout << "Resource from " << __func__ << ": " << p.m_age << endl;
    return 0;
}

输出:

Construct: 1
20
Resource from GetPerson: 000001D5C8D85990
Move construct: 1
Destruct: 1
20
Resource from main: 000001D5C8D85990
Destruct: 2

参考:

  1. 认识 C++ 移动语义与右值引用
  2. 一文读懂 C++ 右值引用和 std::move