C++ 中的rValue的判断---是否能“隐式移动”

C++ 中的"隐式移动"的意思?

先说结论

"隐式移动"指的是:

当一个表达式作为参数、返回值或者初始化对象传递给某个接口时,C++ 编译器根据表达式的值类别,自动选择移动构造函数或移动赋值函数,而不需要程序员手写 std::move

关键词是:

隐式 = 不需要显式调用 std::move


一个具体例子

cpp 复制代码
#include <string>

std::string make()
{
    return std::string("hello");
}

int main()
{
    std::string a = make();        // ①
    std::string b = a;             // ②
    std::string c = std::move(a);  // ③
}

分别分析:


① prvalue 自动触发移动

cpp 复制代码
std::string a = make();

make() 返回的是一个临时对象:

复制代码
prvalue(纯右值)

编译器看到:

cpp 复制代码
string(string&&)

存在移动构造函数。

因此会优先选择:

cpp 复制代码
std::string(std::string&&)

也就是:

移动构造。

程序员没有写:

cpp 复制代码
std::move(...)

但是移动发生了。

这就是:

隐式移动。


② lvalue 只能复制

cpp 复制代码
std::string b = a;

这里:

cpp 复制代码
a

是一个具名变量。

具名变量表达式永远是:

复制代码
lvalue(左值)

因此不能绑定到:

cpp 复制代码
std::string&&

只能选择:

cpp 复制代码
std::string(const std::string&)

也就是:

拷贝构造。


③ std::move 显式触发移动

cpp 复制代码
std::string c = std::move(a);

std::move(a) 的结果:

复制代码
xvalue(将亡值)

xvalue 属于右值:

复制代码
rvalue = xvalue + prvalue

因此可以绑定:

cpp 复制代码
std::string&&

于是调用:

cpp 复制代码
std::string(std::string&&)

发生移动。


"隐式移动"的本质:只是重载决议

很多人觉得移动语义很神秘。

实际上没有任何魔法。

它只是 C++ 的重载匹配规则。

例如:

cpp 复制代码
void f(const T&);
void f(T&&);

当参数是右值

例如:

cpp 复制代码
T obj;

f(std::move(obj));

或者:

cpp 复制代码
f(T());

参数类型:

复制代码
rvalue

可以匹配:

cpp 复制代码
T&&

于是:

复制代码
调用移动版本

当参数是左值

例如:

cpp 复制代码
T obj;

f(obj);

因为:

复制代码
obj 是 lvalue

不能绑定:

cpp 复制代码
T&&

所以只能选择:

cpp 复制代码
const T&

于是:

复制代码
调用拷贝版本

为什么"右值 = 可以隐式移动"?

回到值类别:

复制代码
rvalue = xvalue + prvalue

为什么把 xvalue 和 prvalue 放在一起?

因为它们都有一个共同特点:

可以直接绑定到右值引用 T&&。

例如:

cpp 复制代码
void f(std::string&&);

下面两个都可以:

cpp 复制代码
f(std::string("hello"));   // prvalue

std::string s;
f(std::move(s));           // xvalue

但是:

cpp 复制代码
std::string s;

f(s);   // 错误

因为:

复制代码
s 是 lvalue

不能绑定到:

cpp 复制代码
std::string&&

std::move 到底做了什么?

很多人误认为:

std::move 会把对象里面的数据移动走。

这是错误的。

实际上:

std::move 什么都没有移动。

它本质上只是一个类型转换:

cpp 复制代码
static_cast<T&&>(x)

例如:

cpp 复制代码
std::move(a)

本质类似:

cpp 复制代码
static_cast<std::string&&>(a)

它只是改变表达式的值类别:

复制代码
lvalue
    |
    v
xvalue

真正移动资源的是:

cpp 复制代码
移动构造函数
移动赋值函数

例如:

cpp 复制代码
string(string&& other)
{
    this->ptr = other.ptr;
    other.ptr = nullptr;
}

真正偷走资源的是这里。


一个容易混淆的地方

右值引用变量本身仍然是左值

例如:

cpp 复制代码
int x = 5;

int&& r = std::move(x);

很多人认为:

复制代码
r 是右值

这是错误的。

虽然:

cpp 复制代码
r 的类型是 int&&

但是:

cpp 复制代码
r

这个表达式仍然是:

复制代码
lvalue

所以:

cpp 复制代码
r = 1;

完全合法。


原因:

C++ 规定:所有具名变量,无论类型是什么,作为表达式出现时都是左值。

例如:

cpp 复制代码
void func(std::string&& s)
{
    use(s); 
}

这里:

s

虽然类型:

cpp 复制代码
std::string&&

但是表达式:

cpp 复制代码
s

是:

复制代码
lvalue

所以如果想继续移动:

必须:

cpp 复制代码
use(std::move(s));

这就是为什么移动构造函数里面经常看到:

cpp 复制代码
member = std::move(other.member);

return 中的特殊"隐式移动"

还有一种特殊情况:

cpp 复制代码
std::string func()
{
    std::string s = "hello";

    return s;
}

这里:

s

按照普通规则:

复制代码
lvalue

应该调用拷贝。

但是 C++ 标准特殊规定:

对于返回局部变量:

cpp 复制代码
return s;

编译器会:

  1. 先尝试移动
  2. 移动不可行,再尝试复制

所以可能调用:

cpp 复制代码
string(string&&)

而不是:

cpp 复制代码
string(const string&)

这叫:

复制代码
implicit move(隐式移动)

不建议这样写

很多人会写:

cpp 复制代码
return std::move(s);

实际上通常是不推荐的。

原因:

它可能阻止:

复制代码
NRVO

也就是:

复制代码
返回值优化(Named Return Value Optimization)

本来:

复制代码
直接在目标位置构造对象

现在变成:

复制代码
先移动一次

反而可能降低性能。

推荐:

cpp 复制代码
return s;

让编译器决定。


总结

表达式 值类别 是否自动选择移动
a(具名变量) lvalue ❌ 不移动
std::move(a) xvalue ✅ 移动
make() 返回值 prvalue ✅ 移动
int&& r 中的 r lvalue ❌ 不移动

最后记住一句:

类型是右值引用 T&&,不代表表达式一定是右值;

值类别由表达式决定,而不是由变量类型决定。

这也是 C++ 移动语义中最容易混淆的地方。

复制代码
相关推荐
传奇开心果编程15 小时前
【Rust入门练中学】 第3课:数据类型
开发语言·学习·rust
泡泡鱼(敲代码中)16 小时前
MySQL 学习笔记:DCL、函数与约束 —— 安全、效率、完整性的三板斧
开发语言·笔记·sql·学习·mysql·gitee
I_belong_to_jesus16 小时前
std::unique_ptr成员函数用法
c++
Tairitsu_H17 小时前
[C++] 拷贝构造还是赋值重载?类默认成员函数细节详解
开发语言·c++·类和对象
goodlook012317 小时前
opencode调用本地模型一(1.18.30版本安装)
开发语言
菜鸟~noob23318 小时前
【电子战】第14篇:TOA 定位——圆交汇与到达时间【含matlab代码】
开发语言·matlab
汉克老师18 小时前
GESP2026年9月认证C++四级( 第一部分选择题(1~7题)精讲
c++·gesp·小学生·学c++编程
itmigrate18 小时前
Redis 开发高频隐形踩坑记录
开发语言·java-ee
geats人山人海18 小时前
现代c++第二章 字符串
c++
白远山18 小时前
本地游戏代练源码开发实战:架构设计与核心功能实现指南
java·开发语言·架构·需求分析