Skip to content

左值/右值、移动语义、std::move、完美转发(最高频原理题)

一、左值 vs 右值 + 左值引用 vs 右值引用

左值(lvalue):有名字、可取地址、可持续到语句结束的表达式(变量、*p、数组元素、对象成员…)。 右值(rvalue):临时的、即将销毁、无持久名字的表达式(字面量、临时对象、函数返回临时值…);C++11 又把右值细分 纯右值 prvalue将亡值 xvalue(可被 steal)。

cpp
int a;         // a 左值
a = 42;        // 42、临时右值
&a;            // 可取地址 => 左值
std::move(a);  // 把左值 a 转成右值引用(xvalue)
类型绑定对象特点
T&(左值引用)只能绑左值常用于修改实参、避免拷贝
const T&左值或右值万能只读绑定(也能延长临时生命周期)
T&&(右值引用)只绑右值标志「可窃取资源」,用于移动构造/转发
cpp
void f(int&);        // 左值
void f(int&&);       // 右值
f(x);    // x 左值 -> int&
f(42);   // 右值 -> int&&

二、移动语义(Move Semantics):为什么需要

老 C++:传/返回大对象总是深拷贝(临时对象被拷贝后销毁,浪费)。 C++11 引入移动:拿到一个「即将销毁的右值」时,把它的内部资源(指针/句柄)搬过来,把源置空,O(1) 而非 O(n) 拷贝。

cpp
class Buffer {
    char* p_;
public:
    Buffer(const Buffer& o) {   // 拷贝:深拷贝 O(n)
        p_ = new char[...]; std::copy(...);
    }
    Buffer(Buffer&& o) noexcept : p_(o.p_) {  // 移动:偷指针
        o.p_ = nullptr;      // 源置空,保证其析构安全
    }
};

关键点

  • 移动构造/赋值必须是**带右值引用参数(T&&)**的成员。
  • 源对象移动后应处于「有效但未指定」状态(通常置空/清空),仍可正常析构。
  • 移动通常应 noexcept:这样 vector 扩容才能放心用移动(见 vector 手写里 move_if_noexcept)。

三、std::move 到底是什么?

std::move(x) 并不移动任何东西、不拷贝、不搬数据 —— 它只是 static_cast<T&&>(x)把 x 标记成右值,让后续匹配到移动(&&)函数而触发移动而非拷贝。真正的「搬移」发生在接受 T&& 的移动构造函数/赋值里

cpp
template<typename T>
constexpr std::remove_reference_t<T>&& move(T&& t) noexcept {
    return static_cast<std::remove_reference_t<T>&&>(t);
}

陷阱

  • std::move 之后源对象不应再被使用其值(属"被搬走"状态)。
  • 对内置类型 move 无意义(int 没有移动)。
  • return std::move(local);反优化(编译器本身会按返回右值/移动,加了 move 反而阻碍 NRVO/移动省略)。这是常被用来考的最热门细节。

四、完美转发与引用折叠(进阶必考)

forwarding reference(转发引用 / universal reference)

模板推导出来的 T&&既能绑左值也能绑右值(这是模板特殊规则,与普通 T&& 只能绑右值不同):

cpp
template<typename T> void f(T&& x);   // T 推导:
f(lvalue);   // T = T&    -> 参数变成 T&  (左值引用)
f(rvalue);   // T = T     -> 参数变成 T&& (右值引用)

原因即引用折叠(reference collapsing):T& && → T&T&& & → T&& &→&&& &&→&&。一遇到 & 结果就是 &。

std::forward —— 真正「原样转发」

要让被转发的参数在内部再传给别的重载时保留其左右值身份,不能一律 std::move(那样全变右值),需 std::forward<T>(x)

cpp
template<typename T>
void wrapper(T&& arg) {
    inner(std::forward<T>(arg));  // arg 是左值→转左值;是右值→再转右值
}

move vs forward 记忆

  • std::move(x):无条件转右值。
  • std::forward<T>(x):按推导结果条件性转(还我原貌),专门配转发引用。

万能示例(经典 emplace / make_shared 实现用到这正是因为它用完美转发透传构造参数)

cpp
template<typename T, typename... Args>
T factory(Args&&... args) {
    return T(std::forward<Args>(args)...);   // 好处:参数不被强制拷
}

五、为什么返回移动 / vector 的 emplace/push_back 均摊

  • emplace_back(args...) → 内部完美转发构造。
  • vector::push_back 若接收右值则移动搬入,避免临时深拷贝 → 对非平凡类型内存分配关键。
  • 排序/容器操作大量用到交换/移动能显著提速(std::sort 交换元素用 move)。

六、经典推导测试题(自测用,测脑回路)

cpp
int x;
auto& r1 = x;      // int&
const auto& r2 = x;// const int&
auto&& r3 = x;     // int&(forwarding 规则,绑左值)
auto&& r4 = 42;    // int&&(绑右值)
auto a = x;        // int  (剥引用/const)
decltype(x) b = x; // int  (保留)

答对说明你已理解 auto 剥常引用 与 decltype 原样的区别。

C++ 面试八股 · VitePress 版