C++ std::move 详解
1. 核心概念
std::move 实现的是移动语义(move semantics),用于转移资源所有权,避免昂贵的深拷贝。
关键认知:std::move 本身不移动任何东西 ,它只是一个 static_cast,把左值无条件转成右值引用(T&&),真正的"移动"发生在移动构造/移动赋值函数里。
cpp
// 等价定义
template<typename T>
typename std::remove_reference<T>::type&& move(T &&t) noexcept {
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
2. 基本用法
cpp
QString src = "Hello World";
QString dst = std::move(src); // 调用 QString 的移动构造函数
// src 现在处于"有效但未指定"状态,可析构/可赋值,但不保证内容
qDebug() << src; // 可能是空,可能是原值,不确定
3. 移动构造 / 移动赋值
cpp
class PressureBuffer {
public:
PressureBuffer() = default;
// 移动构造
PressureBuffer(PressureBuffer &&other) noexcept
: m_data(std::move(other.m_data))
, m_size(other.m_size) {
other.m_size = 0;
}
// 移动赋值
PressureBuffer &operator=(PressureBuffer &&other) noexcept {
if (this != &other) {
m_data = std::move(other.m_data);
m_size = other.m_size;
other.m_size = 0;
}
return *this;
}
private:
QVector<double> m_data;
int m_size = 0;
};
注意 :移动操作必须标记 noexcept,否则 STL 容器会退化为拷贝。
4. STL 容器场景(最常用)
场景 1:把局部容器的内容转移出去
cpp
QVector<double> generatePressureData() {
QVector<double> data;
data.reserve(64);
for (int i = 0; i < 64; ++i)
data.push_back(i * 1.5);
return data; // RVO 优先,不需要 std::move
// return std::move(data); // 错误!会禁止 RVO,反而变慢
}
场景 2:把临时对象塞进容器
cpp
QList<SensorData> list;
SensorData data = loadFromDevice();
list.push_back(std::move(data)); // 移动进容器,避免拷贝大数组
// list.push_back(data); // 拷贝,慢
场景 3:把容器内元素转移给另一容器
cpp
QVector<QString> src = {"A", "B", "C"};
QVector<QString> dst;
dst.reserve(src.size());
for (auto &s : src) {
dst.push_back(std::move(s)); // 逐个移动
}
// src 仍是 size=3,但元素内容未指定
5. Qt 场景实战
cpp
void SensorManager::appendData(SensorData data) {
// 按值传入,函数内移动进容器(避免一次拷贝)
m_dataList.push_back(std::move(data));
}
// 调用方
QList<SensorData> batch = readBatch();
m_manager->appendData(std::move(batch)); // 移动整个 list
cpp
// QString 移动
QString build = "very long string ...";
m_label->setText(std::move(build)); // QString 有移动构造,省拷贝
cpp
// QSharedPointer / std::shared_ptr 移动
auto ptr = std::make_shared<Sensor>();
m_currentSensor = std::move(ptr); // 引用计数原子操作,移动比拷贝快
6. 常见陷阱
陷阱 1:移动 const 对象 → 退化为拷贝
cpp
const QString text = "hello";
QString dst = std::move(text); // text 是 const,无法移动,触发拷贝!
陷阱 2:移动基础类型毫无意义
cpp
int a = 10;
int b = std::move(a); // 合法但无意义,int 还是值拷贝
陷阱 3:移动后继续使用源对象
cpp
QString s = "hello";
QString t = std::move(s);
qDebug() << s.length(); // 未定义行为!s 处于"有效但未指定"状态
s = "world"; // 合法,重新赋值后可正常使用
陷阱 4:对返回值用 std::move(最常见错误)
cpp
QVector<double> makeData() {
QVector<double> v = {...};
return std::move(v); // ❌ 禁止 RVO,反而触发移动构造
// ✅ 直接 return v; 编译器做 RVO,零拷贝
}
陷阱 5:移动自身
cpp
x = std::move(x); // 自赋值,可能内存错误
// 标准库实现通常会检查 this == &other
7. 推荐与不推荐场景
| 推荐使用 | 不推荐使用 |
|---|---|
把大对象塞进容器 vec.push_back(std::move(x)) |
基础类型 int、double |
函数参数按值传入,内部移动到成员 m_x = std::move(x) |
return std::move(local)(禁止 RVO) |
| 转移容器所有权(独占资源) | const 对象(退化为拷贝) |
| 替代深拷贝大 QVector / QString / shared_ptr | 移动后继续读源对象 |
智能指针转移 std::unique_ptr |
小于 2-3 个指针的小结构体(拷贝更快) |
8. 关键原则速记
std::move不移动,只是把左值"伪装"成右值- 真正的移动逻辑在移动构造/赋值函数里实现
- 返回局部变量时
return obj;不要加std::move(让 RVO 生效) - 移动操作必须
noexcept(否则 STL 容器退化为拷贝) - 移动后的源对象:可析构、可赋值,不可读
- const 对象无法移动 (
std::move(const T)还是拷贝) - 优先用值传参 + 内部
std::move(如void f(QString s)内部m_x = std::move(s))