一、先弄清几个基本概念
1. 左值(lvalue)与右值(rvalue)
-
左值 :有名字、可取地址、可多次使用的对象。
cppint x = 10; // x 是左值 std::string s = "hello"; // s 是左值 -
右值 :通常是临时对象或字面量,不能取稳定地址,生命周期很短。
cppint y = x + 1; // x+1 是右值 std::string t = s + s; // s+s 产生的临时对象是右值
在没有移动语义之前,C++ 对象在传递、返回、赋值时主要以"拷贝"为主:
- 左值:只能拷贝
- 右值:有时编译器能优化掉拷贝(RVO/NRVO),但整体机制不够统一
2. 右值引用(T&&)
C++11 引入了 右值引用,专门绑定到右值:
cpp
int&& r = 10; // OK,r 绑定到右值 10
int x = 5;
// int&& r2 = x; // 错误,不能把右值引用绑定到左值
有了右值引用,我们就可以为"临时对象"提供专门的构造/赋值逻辑------也就是移动构造函数 和移动赋值运算符。
二、std::move 是什么?
核心一句话:
std::move只是一个类型转换工具 :把一个"左值"显式地转换为对应类型的"右值引用",从而允许调用对象的移动构造/移动赋值函数。
它本身 不做移动操作、不释放资源,真正的"移动"是你或标准库在类的移动构造/赋值中实现的。
实现原型大致是(简化版):
cpp
template <class T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept {
using ReturnType = typename std::remove_reference<T>::type&&;
return static_cast<ReturnType>(t);
}
三、std::move 的典型使用场景
场景 1:编写"移动构造函数"和"移动赋值运算符"
假设有一个简单的资源管理类,内部有一个动态数组指针:
cpp
class Buffer {
public:
Buffer(size_t size)
: size_(size),
data_(new int[size]) {}
// 拷贝构造
Buffer(const Buffer& other)
: size_(other.size_),
data_(new int[other.size_]) {
std::copy(other.data_, other.data_ + other.size_, data_);
}
// 移动构造
Buffer(Buffer&& other) noexcept
: size_(other.size_),
data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
}
// 拷贝赋值
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = new int[size_];
std::copy(other.data_, other.data_ + other.size_, data_);
}
return *this;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data_;
size_ = other.size_;
data_ = other.data_;
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
~Buffer() {
delete[] data_;
}
private:
size_t size_;
int* data_;
};
注意:这里的移动构造/赋值中没有 用到 std::move,因为参数类型已经是 Buffer&& ------它本身就是右值引用。
真正常用 std::move 的地方,是当你要从一个成员(左值)把资源转移给另一个对象时。
例如类里有一个 std::string 成员:
cpp
class Person {
public:
std::string name;
Person(std::string n) : name(std::move(n)) { } // 重点在这里
};
解释:
- 构造函数参数
n是一个局部变量,是左值 (即使它的类型为std::string,依然是左值) - 如果写
name(n),则调用的是std::string的拷贝构造 - 如果写
name(std::move(n)),则调用的是std::string的移动构造(资源"窃取")
场景 2:函数返回值的优化与 std::move
2.1 返回局部对象------一般不需要 std::move
cpp
std::string make_string() {
std::string s = "hello";
return s; // 一般不需要 std::move(s);
}
- C++17 及以后:有强制返回值优化(RVO),
s通常直接在调用者栈帧构造,不发生拷贝/移动 - 即使没有 RVO,编译器也会把
s当作右值,调用移动构造(如果有)
如果你写:
cpp
return std::move(s);
在启用 RVO 的情况下反而可能禁用 RVO,改用移动构造。大多数情况下这是多此一举,可能还更慢。
结论:
- 返回本地局部变量(非
static)时,不要 刻意加std::move,交给 RVO + 移动语义就好。
2.2 返回类成员或参数------某些情况下可以用 std::move
cpp
class Wrapper {
public:
std::string s;
std::string get_and_clear() {
std::string tmp = std::move(s); // 移走成员的资源
s.clear();
return tmp; // 这里交给 RVO/移动
}
};
这里 std::move(s) 用来显式地从成员中偷资源,是合理的。
场景 3:在容器中避免不必要的拷贝
3.1 push_back / emplace_back 与 std::move
cpp
std::vector<std::string> v;
std::string s = "hello";
v.push_back(s); // 拷贝插入
v.push_back(std::move(s)); // 移动插入(s 内容被"搬走",变成空或未定义但可析构状态)
如果你已经不再需要 s 的值,可以用 std::move(s),让插入更高效。
配合 emplace_back:
cpp
v.emplace_back("world"); // 直接原位构造,无需 std::move
v.emplace_back(std::move(s2)); // 同样可以接受右值
注意 :emplace_back 的主要优势是直接原位构造对象,减少临时对象,但参数传右/左值的行为和 push_back 的规则类似。
3.2 容器的 insert/assign 等
cpp
std::vector<std::string> v;
std::string s = "foo";
v.insert(v.begin(), std::move(s)); // 把 s 移动进 vector
当你清楚"这个对象之后不会再用"时,可以用 std::move 提示编译器用移动语义。
场景 4:在通用模板代码中实现完美转发(与 std::forward 配合)
std::move 常和 std::forward 放在一起讲。它们作用不同:
std::move:无条件地把一个表达式转换为右值引用std::forward:在模板中根据原始参数类别(左值/右值)有条件地保持其值类别(完美转发)
典型模式:
cpp
template <typename T, typename... Args>
std::unique_ptr<T> make_object(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
如果在这里错误地使用 std::move:
cpp
T(std::move(args)...) // 会把所有参数都当右值处理,破坏左值参数的语义
所以要记住:
- 模板转发参数:用
std::forward - 你明确知道"不要再用这个对象了":用
std::move
四、std::move 的正确使用习惯与注意事项
1. 使用 std::move 的基本判断
可以问自己两个问题:
- 这个对象接下来在当前作用域里 还会不会被用来读取它的值?
- 目标类型是否支持移动语义(有合适的移动构造/移动赋值函数)?
如果:
- 不再需要读取原值,并且
- 目标类型支持移动
就可以安全使用 std::move。
2. 被 std::move 之后的对象还能用吗?
- 语法上:可以继续使用(可以赋新值、可以析构)
- 语义上:原来的内容已经被"搬走",值处于"有效但未指定状态"
也就是:只能做析构或重新赋值等有限操作,不要依赖其原有内容
示例:
cpp
std::string s = "hello";
std::string t = std::move(s);
// 这里 s 可能为空,也可能是某种内部状态,标准只保证可以安全析构/赋值
s = "world"; // 这是可以的
std::cout << s; // 这时输出 "world"
不要这样做:
cpp
std::string s = "hello";
std::string t = std::move(s);
use(t);
use(s); // 逻辑层面通常是 bug:你已经"声明"不再用 s 了
3. 不要对"本来就会衰变为右值"的临时对象再 std::move
cpp
std::string make();
std::vector<std::string> v;
v.push_back(make()); // 已经是右值,不需要 std::move
v.push_back(std::move(make())); // 多余且无意义(有时还会造成警告)
4. 不要乱对 const 对象使用 std::move
cpp
const std::string s = "hello";
std::string t = std::move(s);
因为 s 是 const,即便使用了 std::move,其类型会变成 const std::string&&,
但移动构造函数通常需要 std::string&&,无法对 const 对象"偷资源"。
结果:还是会调用拷贝构造,既没提高效率,又让代码可读性更差。
一般建议:
- 需要移动的对象不要声明为
const。 - 对
const对象调用std::move基本等同于一个隐晦的const_cast+ 拷贝,应该避免。
5. 与 RVO 的关系再总结一下
-
返回局部对象:
cppT func() { T x; return x; // 轻松交给编译器做 RVO // return std::move(x); // 通常不推荐 } -
返回类的成员或参数时,如果你确实想"偷资源":
cppclass C { T member; public: T steal() { return std::move(member); } };
五、综合示例:从拷贝优化到移动语义
下面是一个展示"有无 std::move 差异"的综合例子。
cpp
#include <iostream>
#include <vector>
#include <string>
class Trackable {
public:
std::string data;
Trackable(const std::string& s) : data(s) {}
Trackable(const Trackable& other)
: data(other.data) {
std::cout << "Copy ctor\n";
}
Trackable(Trackable&& other) noexcept
: data(std::move(other.data)) {
std::cout << "Move ctor\n";
}
Trackable& operator=(const Trackable& other) {
std::cout << "Copy assign\n";
data = other.data;
return *this;
}
Trackable& operator=(Trackable&& other) noexcept {
std::cout << "Move assign\n";
data = std::move(other.data);
return *this;
}
};
int main() {
std::vector<Trackable> v;
std::cout << "---- push_back(lvalue) ----\n";
Trackable a("hello");
v.push_back(a); // 会调用拷贝构造
std::cout << "---- push_back(std::move(lvalue)) ----\n";
Trackable b("world");
v.push_back(std::move(b)); // 会调用移动构造
std::cout << "---- push_back(rvalue) ----\n";
v.push_back(Trackable("tmp")); // 临时对象,调用移动构造(或RVO直接构造在 vector 内部)
}
运行时你通常会看到类似输出:
text
---- push_back(lvalue) ----
Copy ctor
---- push_back(std::move(lvalue)) ----
Move ctor
---- push_back(rvalue) ----
Move ctor // 或者因为优化被省略
这个例子把 std::move 的核心用途展示得很清楚:
把明明是"左值"的对象,以"我不再需要它"的方式交给容器/函数,让它使用移动语义而不是拷贝语义。
六、总结:std::move 的使用要点清单
std::move只是一个强制类型转换,把左值转成右值引用,方便调用移动构造/赋值。- 何时用 :
- 把即将"废弃"的对象传给函数/容器、赋值给另一对象时;
- 在构造函数/赋值运算符中,从参数或成员中"偷"资源;
- 某些需要从成员中 move-out 的返回函数。
- 何时不用 :
- 返回局部变量时(让 RVO 发挥作用);
- 已经是右值/临时对象(如函数直接返回的临时值);
const对象(通常不会带来移动优化)。
- 被
std::move之后的对象要认为是"只能析构/重赋值的壳",不要再依赖其内容。 - 在模板通用代码中要区分:
- 明确"我不再用这个对象了":
std::move - 需要"完美转发实参":
std::forward
- 明确"我不再用这个对象了":