一、右值引用与移动语义

1.1 左值与右值

核心定义

类别 定义 特征
左值(lvalue) 表示数据的表达式,有持久状态,存储在内存中 可以取地址,可以出现在赋值符号左边或右边
右值(rvalue) 字面值常量或表达式求值过程中创建的临时对象 不能取地址,只能出现在赋值符号右边

现代解释

  • lvalue = loactor value:有明确存储地址的对象

  • rvalue = read value:提供数据值但不可寻址的对象

#include <iostream>
using namespace std;

int main() {
    // ========== 左值:可以取地址 ==========
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';

    cout << &c << endl;           // OK
    cout << (void*)&s[0] << endl; // OK

    // ========== 右值:不能取地址 ==========
    double x = 1.1, y = 2.2;

    10;                    // 字面值常量
    x + y;                 // 表达式结果
    fmin(x, y);            // 函数返回值(传值返回)
    string("11111");       // 匿名对象

    // 以下全部编译错误:
    // cout << &10 << endl;
    // cout << &(x+y) << endl;
    // cout << &(fmin(x,y)) << endl;
    // cout << &string("11111") << endl;

    return 0;
}

1.2 左值引用与右值引用

语法对比

引用规则

引用类型 能否引用左值 能否引用右值 方式
左值引用 T& 直接绑定
const 左值引用 const T& 直接绑定
右值引用 T&& 直接绑定
右值引用 T&& ✅(通过 move) move(左值) 或强制转换

#include <iostream>
using namespace std;

int main() {
    int* p = new int(0);
    int b = 1;
    const int c = b;
    *p = 10;
    string s("111111");
    s[0] = 'x';
    double x = 1.1, y = 2.2;

    // ========== 左值引用给左值取别名 ==========
    int& r1 = b;
    int*& r2 = p;
    int& r3 = *p;
    string& r4 = s;
    char& r5 = s[0];

    // ========== 右值引用给右值取别名 ==========
    int&& rr1 = 10;
    double&& rr2 = x + y;
    double&& rr3 = fmin(x, y);
    string&& rr4 = string("11111");

    // ========== const 左值引用引用右值 ==========
    const int& rx1 = 10;
    const double& rx2 = x + y;
    const double& rx3 = fmin(x, y);
    const string& rx4 = string("11111");

    // ========== 右值引用引用 move(左值) ==========
    int&& rrx1 = move(b);
    int*&& rrx2 = move(p);
    int&& rrx3 = move(*p);
    string&& rrx4 = move(s);
    string&& rrx5 = (string&&)s;  // 等价于 move

    // ========== 关键:变量表达式都是左值属性 ==========
    cout << &b << endl;    // OK,b 是左值
    cout << &r1 << endl;    // OK,r1 是左值引用变量,表达式属性是左值
    cout << &rr1 << endl;  // OK,rr1 是右值引用变量,表达式属性是左值!

    // 因此:rr1 的属性是左值,不能再被右值引用绑定,除非 move 一下
    int& r6 = r1;           // OK
    // int&& rrx6 = rr1;    // 错误!rr1 是左值属性
    int&& rrx6 = move(rr1); // OK

    return 0;
}

std::move 的实现

template <class T>
remove_reference_t<T>&& move(T&& _Arg) {
    // forward _Arg as movable
    return static_cast<remove_reference_t<T>&&>(_Arg);
}

本质move 内部是强制类型转换,将左值转换为右值引用。

重要设计:右值引用变量表达式是左值

int&& x = 1;        // x 是右值引用
f(x);               // 调用 f(int& x) —— x 是左值属性!
f(move(x));         // 调用 f(int&& x) —— move 后恢复右值属性

这个设计看似奇怪,但在移动语义的使用场景中非常有价值

1.3 引用延长生命周期

右值引用可用于为临时对象延长生命周期const 左值引用也能做到,但无法修改:

int main() {
    std::string s1 = "Test";

    // std::string&& r1 = s1;          // 错误:不能绑定到左值

    const std::string& r2 = s1 + s1;  // OK:const 左值引用延长生存期
    // r2 += "Test";                  // 错误:不能通过 const 引用修改

    std::string&& r3 = s1 + s1;       // OK:右值引用延长生存期
    r3 += "Test";                      // OK:能通过非 const 引用修改
    std::cout << r3 << '\n';

    return 0;
}

1.4 左值和右值的参数匹配

C++98 的局限

C++11 的精确匹配

#include <iostream>
using namespace std;

void f(int& x) {
    cout << "左值引用重载 f(" << x << ")\n";
}

void f(const int& x) {
    cout << "const 左值引用重载 f(" << x << ")\n";
}

void f(int&& x) {
    cout << "右值引用重载 f(" << x << ")\n";
}

int main() {
    int i = 1;
    const int ci = 2;

    f(i);            // 调用 f(int&) —— 左值
    f(ci);           // 调用 f(const int&) —— const 左值
    f(3);            // 调用 f(int&&) —— 右值
    f(move(i));      // 调用 f(int&&) —— move 后的左值

    int&& x = 1;
    f(x);            // 调用 f(int&) —— x 是左值属性!
    f(move(x));      // 调用 f(int&&) —— move 后恢复右值属性

    return 0;
}

匹配规则总结

实参类型 匹配优先级
左值 T& > const T&
const 左值 const T&
右值 T&& > const T&

1.5 右值引用和移动语义的使用场景

1.5.1 左值引用的局限

左值引用已经解决了大多数场景的拷贝效率问题(传参、传返回值),但有些场景不能使用传左值引用返回

class Solution {
public:
    // 传值返回需要拷贝 —— str 是局部对象,函数结束就销毁
    string addStrings(string num1, string num2) {
        string str;
        // ... 计算逻辑 ...
        return str;  // C++98:只能拷贝;C++11:可以移动!
    }

    // 传值返回代价更大
    vector<vector<int>> generate(int numRows) {
        vector<vector<int>> vv(numRows);
        // ... 计算逻辑 ...
        return vv;
    }
};

问题本质返回对象是局部对象,函数结束就析构销毁了,即使是右值引用返回也无法改变这个事实。

1.5.2 移动构造和移动赋值

定义
函数 参数类型 行为
移动构造 T&&(右值引用) "窃取"右值对象的资源,而非拷贝
移动赋值 T&&(右值引用) "窃取"右值对象的资源,而非拷贝赋值

适用场景

对于 stringvector深拷贝的类,移动构造和移动赋值才有意义:

namespace bit {
class string {
public:
    // 普通构造
    string(const char* str = "")
        : _size(strlen(str)), _capacity(_size) {
        cout << "string(char* str)-构造" << endl;
        _str = new char[_capacity + 1];
        strcpy(_str, str);
    }

    // 拷贝构造 —— 深拷贝
    string(const string& s)
        : _str(nullptr) {
        cout << "string(const string& s)--拷贝构造" << endl;
        _str = new char[s._capacity + 1];
        strcpy(_str, s._str);
        _size = s._size;
        _capacity = s._capacity;
    }

    // ========== 移动构造 —— 窃取资源 ==========
    string(string&& s) {
        cout << "string(string&& s)--移动构造" << endl;
        swap(s);  // 直接交换指针,不拷贝数据!
    }

    // 拷贝赋值
    string& operator=(const string& s) {
        cout << "string& operator=(const string& s)--拷贝赋值" << endl;
        if (this != &s) {
            // ... 深拷贝逻辑 ...
        }
        return *this;
    }

    // ========== 移动赋值 —— 窃取资源 ==========
    string& operator=(string&& s) {
        cout << "string& operator=(string&& s)--移动赋值" << endl;
        swap(s);  // 直接交换指针
        return *this;
    }

    void swap(string& s) {
        ::swap(_str, s._str);
        ::swap(_size, s._size);
        ::swap(_capacity, s._capacity);
    }

    ~string() {
        delete[] _str;
        _str = nullptr;
    }

private:
    char* _str = nullptr;
    size_t _size = 0;
    size_t _capacity = 0;
};
}

int main() {
    bit::string s1("xxxxx");

    bit::string s2 = s1;                    // 拷贝构造
    bit::string s3 = bit::string("yyyyy");  // 构造 + 移动构造,优化后直接构造
    bit::string s4 = move(s1);              // 移动构造 —— s1 的资源被"窃取"

    return 0;
}

移动语义的核心:不是拷贝数据,而是交换指针,让原对象交出资源所有权。

1.5.3 解决传值返回问题

namespace bit {
    string addStrings(string num1, string num2) {
        string str;
        // ... 计算 ...
        return str;
    }
}

// 场景1:用返回值构造对象
int main() {
    bit::string ret = bit::addStrings("11111", "2222");
    cout << ret.c_str() << endl;
    return 0;
}

// 场景2:赋值给已有对象
int main() {
    bit::string ret;
    ret = bit::addStrings("11111", "2222");
    cout << ret.c_str() << endl;
    return 0;
}

编译器优化演进

环境 无移动语义 有移动语义 现代编译器优化
VS2019 Debug 2 次拷贝构造 2 次移动构造
VS2019 Release / VS2022 合三为一,直接构造
Linux (g++ -fno-elide-constructors) 2 次拷贝 2 次移动
  1. 无移动语义 + 无优化str → 拷贝构造 → 临时对象 → 拷贝构造 → ret

  2. 有移动语义 + 无优化str → 移动构造 → 临时对象 → 移动构造 → ret

  3. 现代编译器优化(NRVO/RVO):直接在 ret 的内存位置构造 str,零拷贝!

右值对象构造,只有拷贝构造,没有移动构造的场景 

右值对象构造,只有拷贝构造,也有移动构造的场景 

右值对象构造,只有拷贝构造和拷贝复制,没有移动构造的移动赋值场景 

右值对象构造,只有拷贝构造和拷贝复制,也有移动构造的移动赋值场景 

赋值场景的优化
bit::string ret;
ret = bit::addStrings("11111", "2222");
  • 无移动语义:拷贝构造临时对象 + 拷贝赋值给 ret

  • 有移动语义移动构造临时对象 + 移动赋值给 ret

  • 现代编译器:直接构造临时对象,str 本质是其引用,底层用指针实现

3.5.4 在传参中的提效

C++11 以后,容器的 pushinsert 系列接口增加了右值引用版本:

// STL 新增接口
void push_back(const value_type& val);   // 左值版本
void push_back(value_type&& val);         // 右值版本
iterator insert(const_iterator position, value_type&& val);

1.6 类型分类

C++11 对值类别进行了更精细的划分:

类别 全称 说明 示例
glvalue generalized value(泛左值) 有身份的对象 包含 lvalue 和 xvalue
lvalue locator value 可寻址的持久对象 变量名、*ps[0]
xvalue expiring value(将亡值) 即将被移动的对象 move(x)static_cast<X&&>(x)
prvalue pure rvalue(纯右值) 纯临时值 42a+bstr.substr(1,2)
rvalue 包含 xvalue 和 prvalue

C++98 的右值 ≈ C++11 的 prvalue

1.7 引用折叠

++ 不能直接定义引用的引用(int&&& r = i; 报错),但通过模板或 typedef 可以构成引用的引用:

规则右值引用的右值引用折叠成右值引用所有其他组合折叠成左值引用

   

类型组合 折叠结果
T& & T&
T& && T&
T&& & T&
T&& && T&&
typedef int& lref;
typedef int&& rref;

int n = 0;

lref& r1 = n;    // int& &  → int&
lref&& r2 = n;   // int& && → int&
rref& r3 = n;    // int&& & → int&
rref&& r4 = 1;   // int&& && → int&&

万能引用(Universal Reference)

万能引用的判定条件

  1. 必须是函数模板参数

  2. 必须是 T&& 形式(const T&& 不是万能引用)

template<class T>
void f1(T& x) {}  // 总是左值引用

template<class T>
void f2(T&& x) {}  // 可能是左值引用或右值引用

int main() {
    int n = 0;

    // f1 实例化
    f1<int>(n);     // void f1(int& x) —— 无折叠
    // f1<int>(0);  // 错误!右值不能绑定到左值引用

    f1<int&>(n);    // void f1(int& x) —— int& & → int&
    f1<int&&>(n);   // void f1(int& x) —— int&& & → int&

    // f2 实例化
    f2<int>(n);     // 错误!void f2(int&& x),左值不能绑定
    f2<int>(0);     // OK!void f2(int&& x)

    f2<int&>(n);    // void f2(int& x) —— int& && → int&
    // f2<int&>(0);  // 错误!

    f2<int&&>(n);   // 错误!void f2(int&& x),左值不能绑定
    f2<int&&>(0);   // OK!void f2(int&& x) —— int&& && → int&&
}

1.8 完美转发

void Fun(int& x)      { cout << "左值引用" << endl; }
void Fun(const int& x){ cout << "const 左值引用" << endl; }
void Fun(int&& x)     { cout << "右值引用" << endl; }
void Fun(const int&& x){ cout << "const 右值引用" << endl; }

template<class T>
void Function(T&& t) {  // 万能引用
    Fun(t);  // 问题:t 是变量,表达式属性是左值!
             // 无论传进来的是左值还是右值,都匹配 Fun(int&)
}

问题根源:变量表达式都是左值属性,右值被右值引用绑定后,右值引用变量 t 的属性变成了左值。

std::forward 实现

template <class _Ty>
_Ty&& forward(remove_reference_t<_Ty>& _Arg) noexcept {
    // forward an lvalue as either an lvalue or an rvalue
    return static_cast<_Ty&&>(_Arg);
}

原理

  • 实参是右值 → T 推导为 intforward<int>(t)static_cast<int&&>(t) → 返回右值引用

  • 实参是左值 → T 推导为 int&forward<int&>(t)static_cast<int& &&>(t) → 折叠为 int& → 返回左值引用

template<class T>
void Function(T&& t) {
    Fun(forward<T>(t));  // 保持 t 的原始属性
}

int main() {
    Function(10);           // 右值 → Fun(int&&)

    int a;
    Function(a);            // 左值 → Fun(int&)

    Function(move(a));      // 右值 → Fun(int&&)

    const int b = 8;
    Function(b);            // const 左值 → Fun(const int&)
    Function(move(b));      // const 右值 → Fun(const int&&)

    return 0;
}

完美转发的使用场景
  1. 模板函数中传递参数保持参数的原始值类别

  2. 工厂函数make_uniquemake_shared

  3. emplace 系列接口:将参数包完美转发给构造函数

template <class... Args>
void emplace_back(Args&&... args) {
    insert(end(), forward<Args>(args)...);  // 完美转发参数包
}

特性 C++98 C++11 核心价值
初始化方式 ()= 不统一 {} 统一初始化 语法统一,防止窄化转换
容器多值初始化 不支持 std::initializer_list 简洁、高效
传值返回优化 只能拷贝 移动语义 避免深拷贝,性能提升显著
参数传递 左值引用/传值 右值引用 + 完美转发 按需选择拷贝或移动
模板编程 固定参数 可变参数模板 更灵活的泛型编程

更多推荐