6人参与 • 2025-12-11 • C/C++
传统 c++ 的对象拷贝(copy)在管理资源(堆内存、文件句柄、套接字、大数组等)时代价高。以前的做法:
移动语义的目标:当可以“窃取”一个临时对象的内部资源而不是逐元素复制时,允许编译器选择把资源从源对象“移动”到目标对象,使得构造与赋值的成本从 o(n) 变为 o(1)。这是通过 右值引用(t&&) 与专门的 移动构造函数 / 移动赋值运算符 实现的。
在 c++11 之前:
std::vector<pointxyz>)会发生昂贵的 深拷贝(deep copy)。因此:
std::vector<int> v; // … auto x = v; // 必须深拷贝
深拷贝非常昂贵,尤其对于 slam 中:
右值引用 + 移动语义 就是解决上述性能瓶颈的关键。
| 类别 | 描述 | 示例 |
|---|---|---|
| lvalue(左值) | 有名字,可取地址 | x、v[0] |
| xvalue(将亡值) | 即将被销毁的对象,可以“偷资源” | std::move(x)、t&& 某些表达式 |
| prvalue(纯右值) | 临时值,无名称 | t()、3、func() 返回临时 |
| glvalue(泛左值) | lvalue + xvalue | 能代表 “对象的定位” |
| rvalue(右值) | xvalue + prvalue | 可移动但不能取地址 |
记住要点:
右值引用就是绑定 xvalue 与 prvalue 的一种引用。
关键:右值引用 t&& 能接受 xvalue/prvalue,但不接受 lvalue(除非用 std::move 或模板完美转发)。
对类 t,推荐实现(rule of five):
struct t {
// 构造/析构
t(); // default ctor
t(const t&); // copy ctor
t(t&&) noexcept; // move ctor
t& operator=(const t&); // copy assign
t& operator=(t&&) noexcept; // move assign
~t();
};
示例 — 简单资源类(动态数组)
#include <iostream>
#include <utility> // std::move
struct buffer {
size_t size_;
double* data_;
buffer(size_t n=0) : size_(n), data_(n ? new double[n] : nullptr) {}
~buffer() { delete[] data_; }
// copy
buffer(const buffer& o) : size_(o.size_) {
if (size_) {
data_ = new double[size_];
std::copy(o.data_, o.data_ + size_, data_);
} else data_ = nullptr;
std::cout<<"copy ctor\n";
}
// move
buffer(buffer&& o) noexcept : size_(o.size_), data_(o.data_) {
o.size_ = 0;
o.data_ = nullptr;
std::cout<<"move ctor\n";
}
// copy assign
buffer& operator=(const buffer& o){
if(this==&o) return *this;
delete[] data_;
size_=o.size_;
data_ = size_? new double[size_]: nullptr;
std::copy(o.data_, o.data_ + size_, data_);
std::cout<<"copy assign\n";
return *this;
}
// move assign
buffer& operator=(buffer&& o) noexcept {
if(this==&o) return *this;
delete[] data_;
size_ = o.size_;
data_ = o.data_;
o.size_ = 0;
o.data_ = nullptr;
std::cout<<"move assign\n";
return *this;
}
};要点:
nullptr、0)。noexcept 很重要 —— 它允许容器(比如 std::vector<t>)在扩容时使用移动而不是拷贝(容器会在异常安全性不保证时退回拷贝策略)。std::move(x):把 lvalue 强制转换成 xvalue(右值),告诉编译器“可以窃取 x 的资源”。但它本身不移动;只是类型转换。std::forward<t>(x):在模板中保留值类别(完美转发)。当模板参数 t 是 u&& 的情况下,forward 会在 t 为 lvalue-reference 时转成 lvalue,否则转成 rvalue。示例:
void take_by_value(buffer b) { /*...*/ }
buffer b(100);
take_by_value(b); // copy
take_by_value(std::move(b)); // move (resource stolen)注意:对已 std::move 的对象继续使用可能导致未定义语义(安全但不可预测状态),称为 use-after-move。移动后的对象必须处于析构与赋值安全状态,但其具体内容不可依赖,除非类型指定了明确语义。
在很多情况下函数返回临时对象时会有拷贝或移动。现代编译器会做 (命名)返回值优化(rvo / nrvo),避免额外拷贝/移动。c++17 更严格地把 prvalue 语义演进,使得通常不会触发移动或拷贝(直接在调用者处构造返回对象)。
示例:
buffer make_buffer(size_t n){
buffer tmp(n);
// ... fill ...
return tmp; // rvo: tmp 在调用处直接构造
}
即便没有 rvo,若 buffer 有移动构造,也会用移动构造移动临时对象(开销小)。
std::vector、std::deque 等)会在重新分配时尽量移动元素(若 t 的移动构造 noexcept,容器使用移动;否则可能回退到拷贝)。因此,务必对能移动的大对象提供 noexcept 的移动构造/赋值。std::move_iterator、std::make_move_iterator 可以把算法变为“移动模式”:例如 std::copy(std::make_move_iterator(first), std::make_move_iterator(last), dest) 会把元素移动到 dest。std::move_if_noexcept:在条件下选择移动(如果移动抛异常则拷贝)。容器扩容等场景常使用这一策略。noexcept。若必须在移动中做可能抛异常的操作,可以将那些操作放在拷贝 / swap 路径中,或保证内部操作不会抛异常。推荐模式(move via swap):
t& operator=(t&& other) noexcept {
swap(*this, other);
return *this;
}但要确保 swap 本身 noexcept。
移动后对象应该处于有效但未指定内容的状态,可安全析构和赋值,但不能假设其值。文档化被移动后对象的可用操作(建议仅能被赋值或析构)是一种好习惯。
如果类管理不可转移资源(例如与 os 绑定的唯一句柄,或禁止移动的语义),可以删掉移动构造:
t(t&&) = delete; t& operator=(t&&) = delete;
对小 pod(如 int, double)移动没有意义;移动语义主要针对“外部资源”。但实现移动构造不会有坏处,只是多写一点代码。
如果用户定义了任一种:析构(dtor)、拷贝构造、拷贝赋值、移动构造、移动赋值,应考虑同时实现或禁用另外两者以避免编译器生成不合适的默认函数。
对于函数模板,重载 f(const t&) 与 f(t&&) 时注意:f(t&&) 对于左值不会匹配除非使用模板参数推导或 std::move。这常用于实现 emplace_back 或工厂函数。
std::unique_ptr 是只可移动不可拷贝的典型例子。它利用移动语义保证资源唯一性。使用 unique_ptr 可以安全地把资源传递给函数或容器(容器会移动 unique_ptr 对象)。
std::unique_ptr<foo> make_foo() {
return std::make_unique<foo>();
}
std::vector<std::unique_ptr<foo>> v;
v.push_back(make_foo()); // move into vector注意:std::vector<t> 可以存放 move-only 类型(c++11 起)。
完美转发 用于把参数原样传递给构造函数或函数,避免不必要拷贝/移动:
template<typename t, typename... args>
std::unique_ptr<t> make_unique_impl(args&&... args){
return std::unique_ptr<t>(new t(std::forward<args>(args)...));
}std::vector::emplace_back(args...) 使用完美转发在目标存储处直接构造对象,避免临时对象再移动/拷贝(相对于 push_back(t(args...)) 更高效)。
noexcept,std::vector 可能在扩容时拷贝元素造成意外开销。检查移动构造是否 noexcept。std::move:对返回局部变量的 return std::move(local) 会阻止 rvo,并且通常会产生多余的 move。不要在 return 语句中对返回值使用 std::move(c++11/14 中反模式,c++17 prvalue 语义进一步缓解)。大型数学库(eigen)使用表达式模板避免临时对象。移动语义配合表达式模板可以极大减少分配和复制。
当对象使用自定义内存池(固定内存区域)时,移动构造往往只是指针/偏移值的复制,性能几乎是常数。
在多线程场景中移动对象时要注意竞争:移动操作不是线程安全的;在移动前应保证没有其他线程同时访问/修改该对象。
std::function 在 c++11 中要求可拷贝目标;若要传递 unique_ptr 到回调,可使用 std::move 包装 lambda 捕获: auto cb = [p = std::move(ptr)](){ ... };。c++17/20 中有更多灵活性(std::move_only_function 提案/实现)。
noexcept(如果安全)。t&& 或模板转发参数以便移动(如 push_back(t&&) / emplace_back)。std::move_if_noexcept 或合理选择异常策略,以保证强异常安全。return 中使用 std::move(local)(破坏 rvo)。std::unique_ptr 作为首选的 movable-only 智能指针(比裸指针安全)。下面是一个短示例展示 std::vector<buffer> 在 reallocation 时如何受 noexcept 影响:
#include <vector>
#include <iostream>
struct noexceptbuffer {
noexceptbuffer(noexceptbuffer&&) noexcept { }
noexceptbuffer& operator=(noexceptbuffer&&) noexcept { return *this; }
noexceptbuffer() = default;
};
struct maythrowbuffer {
maythrowbuffer(maythrowbuffer&&) { } // not noexcept
maythrowbuffer& operator=(maythrowbuffer&&) { return *this; }
maythrowbuffer() = default;
};
int main(){
std::vector<noexceptbuffer> v1;
v1.reserve(100);
for(int i=0;i<100;++i) v1.emplace_back();
std::vector<maythrowbuffer> v2;
v2.reserve(100);
for(int i=0;i<100;++i) v2.emplace_back();
std::cout<<"done\n";
}在某些实现中,v1 在扩容时会将元素移动(更快),而 v2 因移动可能抛异常,会退回到拷贝(或触发更复杂安全性处理),性能差异明显。
t&& 与 std::move / std::forward 是实现移动语义与完美转发的基础工具。noexcept。noexcept 的设计会影响库级别性能。unique_ptr、emplace、make_move_iterator、完美转发等现代 c++ 工具编写高性能、异常安全的代码。到此这篇关于c++ 右值引用(rvalue references)与移动语义(move semantics)深度详解的文章就介绍到这了,更多相关c++ 右值引用与移动语义内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论