左值和右值的本质区别在于能否取地址:&e合法则为左值,否则通常为右值;这是编译期确定的值类别,影响绑定、移动等行为。

左值和右值的本质区别在于能否取地址
判断一个表达式是左值还是右值,最直接的方法是看能不能对它取地址:&e 是否合法。如果能,就是左值;否则通常是右值(极少数例外如位域成员,但日常几乎不遇)。这不是语法分类,而是表达式的“值类别”(value category),影响绑定、移动、函数重载等行为。
常见误区是认为“有名字的就是左值”,但其实:临时对象有名字也是右值,比如 auto&& x = std::string("hello"); 中的 x 是左值引用,但绑定的是右值;而 std::string("hello") 本身没名字,是纯右值(prvalue)。
如何用 decltype 精确查看表达式的值类别
decltype 是调试左值/右值最可靠的工具。它对表达式的推导规则严格反映值类别:
-
decltype(x)(变量名)→ 类型为T(左值) -
decltype((x))(加括号)→ 类型为T&(强制视为左值表达式) -
decltype(std::move(x))→ 类型为T&&(右值引用) -
decltype(42)→ 类型为int(prvalue,无引用)
例如:
int i = 0;<br>static_assert(std::is_same_v<decltype(i), int>); // true<br>static_assert(std::is_same_v<decltype((i)), int&>); // true<br>static_assert(std::is_same_v<decltype(1+2), int>); // true括号改变表达式性质,这是手动“降级”为左值的常用技巧。
立即学习“C++免费学习笔记(深入)”;
函数返回类型如何决定返回值的值类别
函数返回值是不是左值,只取决于返回类型和是否命名,和 return 表达式无关:
- 返回
T(非引用)→ 总是 prvalue(如std::string f() { return "abc"; }返回纯右值) - 返回
T&→ 返回值是左值(即使 return 的是临时对象,也可能引发 dangling reference) - 返回
T&&→ 返回值是 xvalue(将亡值),属于右值的一种
注意:C++17 引入了强制拷贝省略(guaranteed copy elision),但值类别判定仍按标准规则来,不因优化而改变。比如 T f() { return T{}; } 中的 T{} 是 prvalue,f() 调用结果仍是 prvalue,只是构造可能被省略。
移动语义中常见的右值误判场景
写 std::move 不等于“变成右值”,它只是把左值**显式转为右值引用类型**,本质是 static_cast<t></t>。真正触发移动,还需要目标类型有匹配的右值引用重载,且对象未被 move 过。
- 对 const 左值调用
std::move→ 得到const T&&,通常无法绑定到T&&构造函数,移动失败(退化为拷贝) - 对已 move 的对象再次 move → 行为未定义(内容可能为空或垃圾,但类型仍是右值)
- 返回局部对象时写
return std::move(local);→ 反而抑制 NRVO,降低性能,应直接return local;
真正需要 std::move 的地方,是当你明确持有某个具名对象(左值),又想把它当作资源移交出去时,比如在移动构造函数里转发成员:
Widget(Widget&& other) : data_(std::move(other.data_)) {}
值类别的判断不依赖运行时状态,全是编译期确定的。最容易忽略的是括号对表达式值类别的改变,以及 decltype 和实际使用时绑定规则的差异——比如 T&& 参数在函数体内是左值(有名字),必须再用 std::move 才能继续传递为右值。









