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;
编译器会:
- 先尝试移动
- 移动不可行,再尝试复制
所以可能调用:
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++ 移动语义中最容易混淆的地方。