移动语义(Move Semantics)
C++11 引入的移动语义解决了一个核心问题:如何在传递大对象时避免不必要的深拷贝。
1. 概念:拷贝 vs 移动
1.1 拷贝——"复印一份"
std::vector<int> a = {1, 2, 3, 4, 5}; // a 拥有这 5 个 int
std::vector<int> b = a; // 深拷贝:b 分配新内存,把 5 个 int 逐一遍过去
// 现在 a 和 b 各有一份数据,互不影响
拷贝前: a ──→ [1][2][3][4][5] (在地址 0x1000)
拷贝后: a ──→ [1][2][3][4][5] (仍在 0x1000)
b ──→ [1][2][3][4][5] (新分配 0x2000,数据完全独立)
1.2 移动——"把资源偷过来"
std::vector<int> a = {1, 2, 3, 4, 5};
std::vector<int> b = std::move(a); // 移动:b 直接接管 a 的指针,a 被"掏空"
// a 现在是空的,数据的所有权转移给了 b
移动前: a ──→ [1][2][3][4][5] (在地址 0x1000)
移动后: b ──→ [1][2][3][4][5] (仍指向 0x1000,没有分配新内存)
a ──→ (空)
关键区别:移动没有分配新内存,只是把指针从 a 换到 b。
1.3 什么时候移动比拷贝划算
| 对象类型 | 拷贝代价 | 移动代价 | 移动值不值得 |
|---|---|---|---|
int、float 等基本类型 |
1 条 mov 指令 | 1 条 mov 指令 | ❌ 一样,没区别 |
std::vector<int>(1000) |
分配内存 + 复制 1000 个元素 | 交换 3 个指针 | ✅ 巨大差别 |
Eigen 矩阵 MatrixXf(100,100) |
分配 10000 个 float + 逐元素复制 | 交换内部指针 | ✅ 巨大差别 |
std::unique_ptr<T> |
❌ 不允许拷贝 | ✅ 移动 | ✅ 必须移动(unique_ptr 不可拷贝) |
2. 工程代码分析(GSRL 中的场景)
2.1 哪些对象适合移动
GSRL 中存在大量"重量级"对象,拷贝它们代价很高:
// KalmanFilter 内部持有 Eigen 矩阵——拷贝 = 全矩阵深拷贝
KalmanFilter<fp32, 3, 1, 0> kf;
kf.predict(); // 这个操作涉及矩阵乘法
// RLS 滤波器同样持有协方差矩阵
RLSFilter4D rls;
rls.update(input, output);
KalmanFilter<fp32, 3, 1, 0> 包含的成员:
├── StateVector m_state (Eigen::Vector<fp32, 3>)
├── StateMatrix m_covariance (Eigen::Matrix<fp32, 3, 3>)
├── StateMatrix m_transition (Eigen::Matrix<fp32, 3, 3>)
├── StateMatrix m_processNoise (Eigen::Matrix<fp32, 3, 3>)
├── ObsMatrix m_observation (Eigen::Matrix<fp32, 1, 3>)
├── MeasMatrix m_measNoise (Eigen::Matrix<fp32, 1, 1>)
├── ControlMatrix m_control (Eigen::Matrix<fp32, 3, 0>)
└── GainMatrix m_gain (Eigen::Matrix<fp32, 3, 1>)
默认拷贝 → 全部矩阵深拷贝 → 分配内存 + 逐元素复制
移动语义 → 只交换内部指针 → 几乎零开销
2.2 GSRL 中的实际使用
GSRL 依赖的 Eigen 数学库完整支持移动语义。当你在项目中编写类似下面的代码时,移动语义就自动生效了:
// 场景:函数返回一个配置好的滤波器
KalmanFilter2D createPosVelKF(fp32 dt)
{
KalmanFilter2D kf; // 临时对象
kf.setStateTransition(/* F 矩阵 */);
kf.setObservationMatrix(/* H 矩阵 */);
return kf; // ✅ 编译器自动使用移动语义(RVO/移动构造)
// 不会拷贝整个滤波器!
}
// 场景:将滤波器存入容器
class MultiSensorFusion
{
std::vector<KalmanFilter2D> filters;
void addFilter(KalmanFilter2D &&kf)
{
filters.push_back(std::move(kf)); // 移动进容器,不拷贝
}
};
2.3 行为树中的潜在场景
GSRL 的行为树用 memset 初始化子节点数组:
// alg_behavior_tree.hpp:85-88
template <uint8_t MaxChildren>
class BTComposite : public BTNode
{
protected:
BTNode *m_children[MaxChildren]; // 存储的是指针,不是对象
public:
BTComposite()
{
memset(static_cast<void *>(m_children), 0, sizeof(m_children));
}
};
这里存的是裸指针(BTNode *),不涉及移动语义。但如果将来想升级成 std::vector<std::unique_ptr<BTNode>>,就必须用 std::move:
// 升级版:用智能指针管理节点生命周期
class BTCompositeV2 : public BTNode
{
protected:
std::vector<std::unique_ptr<BTNode>> m_children;
public:
// unique_ptr 不可拷贝,必须用移动
bool addChild(std::unique_ptr<BTNode> child)
{
if (child == nullptr) return false;
m_children.push_back(std::move(child)); // 移动所有权
return true;
}
};
3. 动手写:在你的代码中使用 std::move
3.1 示例1:将滤波器存入容器
结合 GSRL 的 KalmanFilter 使用场景:
#include <vector>
#include <utility> // std::move 在这里
// 场景:一个类管理多个滤波器
class FusionManager
{
public:
std::vector<KalmanFilter2D> filters;
// 添加滤波器:接收右值引用
void addFilter(KalmanFilter2D &&filter)
{
// std::move 将 filter 转成右值,触发移动构造而不是拷贝构造
filters.push_back(std::move(filter));
}
};
// 使用
int main()
{
FusionManager manager;
// 创建一个配置好的滤波器
KalmanFilter2D kf = createPosVelKF(0.001f);
// 移入管理器——不会深拷贝整个滤波器!
manager.addFilter(std::move(kf));
// ⚠️ 此时 kf 已经被"掏空",不要再使用它
// kf.predict(xxx); // 未定义行为!
}
3.2 示例2:PID 控制器的移动
// 定义一个支持移动语义的 PID 控制器
class SimplePID
{
public:
struct PIDParam {
fp32 Kp, Ki, Kd;
fp32 outputLimit;
fp32 integralLimit;
};
private:
PIDParam m_param;
fp32 *m_errorHistory; // 动态分配的误差历史数组
int m_historySize;
public:
// 构造函数:分配动态内存
SimplePID(const PIDParam ¶m, int historySize = 100)
: m_param(param), m_historySize(historySize)
{
m_errorHistory = new fp32[historySize];
memset(m_errorHistory, 0, historySize * sizeof(fp32));
}
// 移动构造函数:偷走资源,不重新分配
SimplePID(SimplePID &&other) noexcept
: m_param(other.m_param)
, m_errorHistory(other.m_errorHistory) // 直接接管指针
, m_historySize(other.m_historySize)
{
// 把原对象的指针置空,防止析构时 double-free
other.m_errorHistory = nullptr;
other.m_historySize = 0;
}
// 移动赋值运算符
SimplePID &operator=(SimplePID &&other) noexcept
{
if (this != &other) {
delete[] m_errorHistory; // 释放自己的旧资源
m_errorHistory = other.m_errorHistory; // 接管对方的资源
m_historySize = other.m_historySize;
m_param = other.m_param;
other.m_errorHistory = nullptr;
other.m_historySize = 0;
}
return *this;
}
// 禁止拷贝(或者实现深拷贝)
SimplePID(const SimplePID &) = delete;
SimplePID &operator=(const SimplePID &) = delete;
// ↑ = delete 是 C++11 语法,显式声明"这个函数不存在"
// 如果有人尝试拷贝 SimplePID,会在编译期直接报错,而不是运行时崩
// 析构函数——对象销毁时自动调用,负责清理动态分配的资源
// 复习:如果 m_errorHistory 在移动时已被置为 nullptr,delete[] nullptr 是安全的(空操作)
~SimplePID()
{
delete[] m_errorHistory;
}
};
// 使用
int main()
{
SimplePID::PIDParam param = {10.0f, 0.1f, 0.0f, 100.0f, 50.0f};
SimplePID pid1(param); // 创建 PID1
SimplePID pid2(std::move(pid1)); // 移动构造——pid1 的数组被 pid2 接管
// pid1 不能再用了,它的内部指针已经被置空
}
伪代码——移动过程中发生了什么:
移动前:
pid1 ──→ m_errorHistory ──→ [0][0][0]...[0] (100 个 fp32,在堆上)
pid2 ──→ m_errorHistory ──→ nullptr
SimplePID pid2(std::move(pid1)); 执行后:
pid1 ──→ m_errorHistory ──→ nullptr ← 被掏空
pid2 ──→ m_errorHistory ──→ [0][0][0]...[0] ← 接管了原 pid1 的数据
没有分配新内存!只是复制了一个指针。
3.3 示例3:常见错误
void badExample()
{
std::vector<int> data = {1, 2, 3, 4, 5};
// 错误1:std::move 之后继续使用原对象
std::vector<int> other = std::move(data);
// data.push_back(6); // ❌ data 处于"有效但未定义"状态
// 可能崩溃,可能不崩,但一定是错的
// 原因:vector 内部的数据存在堆上。std::move 后 data 的内部指针已被置空,
// 此时再访问就相当于对空指针解引用。详见《内存.md》的栈/堆部分。
// 正确做法:永远假设被 move 的对象已经失效
// 如果还想用 data,先重新赋值:
data = {10, 20, 30}; // ✅ 重新赋值后可以正常使用
// 错误2:试图移动 const 对象
const std::vector<int> constVec = {1, 2, 3};
// std::vector<int> v = std::move(constVec);
// ❌ std::move(constVec) 返回的是 const 右值引用
// 不会匹配移动构造函数(移动要修改源对象),
// 而是回退到拷贝构造——搬起石头砸自己的脚
}
4. std::move 本质
std::move 本身不移动任何东西!
它的作用只有一件事:
把左值强制转换成右值引用 → 让编译器选择移动重载(移动构造/移动赋值)
真正的"移动"操作发生在:
目标类的移动构造函数 / 移动赋值运算符 中
std::move(x) ≈ static_cast<T&&>(x)
一句话总结:std::move 是"我想移动这个对象"的意图声明,实际移动由类的移动构造函数完成。
5. 使用原则
| 原则 | 说明 |
|---|---|
大对象用 std::move 传 |
Eigen 矩阵、std::vector、std::string 等 |
unique_ptr 必须 std::move |
它根本不能拷贝 |
基本类型不要 std::move |
int、float 移动 = 拷贝,多写无益 |
std::move 后不要用原对象 |
除非你确定它的状态(如重新赋值后) |
const 对象不要 std::move |
实际会回退成拷贝 |
返回局部变量时不需要 std::move |
编译器自动做 RVO/NRVO |
6. 总结
| 概念 | 拷贝 | 移动 |
|---|---|---|
| 操作 | 把数据逐份复制 | 把资源所有权转移 |
| 内存 | 分配新空间 | 不分配 |
| 原对象 | 保持完整 | 被"掏空"(有效但内容不确定) |
int/float |
没差别 | 没差别 |
vector/Matrix |
很慢 | 很快 |
unique_ptr(独占所有权的智能指针,不可拷贝) |
不允许 | 必须 |
作者: Qing | 修改日期: 2026-07-18