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++ 移动语义中最容易混淆的地方。

复制代码
相关推荐
xxy2224118 分钟前
TCP 网络编程(3):从基础通信到并发服务器与 HTTP 实战
开发语言·php
五_谷_丰_登22 分钟前
平衡二叉搜索树讲解
数据结构·c++
Discipline~Hai25 分钟前
Linux应用软件编程04-线程
linux·服务器·c语言·开发语言·linux应用软件编程
星星.72225 分钟前
【图论】最小生成树|Prim+Kruskal算法
数据结构·c++·算法·图论
charlie11451419130 分钟前
Cinux · musl 静态移植:对齐 Linux ABI、铺初始栈,以及一个被 SMAP 拦下的潜伏 bug
linux·开发语言·c++·操作系统·开源项目
_Narcissus_40 分钟前
单调栈笔记及例题详解
数据结构·c++·笔记·算法·力扣·单调栈·洛谷
流浪0011 小时前
C/C++后端筑基系列(二):C++ 从基础到进阶
开发语言·c++
kyle~1 小时前
x86 汇编LOCK前缀 --- 硬件架构向软件提供的核心同步原语
汇编·c++·性能优化·硬件架构·实时系统