C++ 右值引用(rvalue references)與移動語義(move semantics)深度解析
一、右值引用(rvalue references)與移動語義(move semantics)設(shè)計動機
1.1 為什么需要移動語義
傳統(tǒng) C++ 的對象拷貝(copy)在管理資源(堆內(nèi)存、文件句柄、套接字、大數(shù)組等)時代價高。以前的做法:
- 提供拷貝構(gòu)造/賦值,或
- 使用指針/引用共享資源(容易出錯)或
- 使用 swap 優(yōu)化復(fù)制(代價仍然存在)
移動語義的目標:當(dāng)可以“竊取”一個臨時對象的內(nèi)部資源而不是逐元素復(fù)制時,允許編譯器選擇把資源從源對象“移動”到目標對象,使得構(gòu)造與賦值的成本從 O(n) 變?yōu)?O(1)。這是通過 右值引用(T&&) 與專門的 移動構(gòu)造函數(shù) / 移動賦值運算符 實現(xiàn)的。
1.2 為什么需要右值引用
在 C++11 之前:
- 返回大對象(如
std::vector<PointXYZ>)會發(fā)生昂貴的 深拷貝(deep copy)。 - 臨時對象往往不需要被復(fù)制(因為臨時對象馬上就會銷毀)。
- 無法區(qū)分 “臨時對象的引用” 與 “普通左值引用”。
因此:
std::vector<int> v; // … auto x = v; // 必須深拷貝
深拷貝非常昂貴,尤其對于 SLAM 中:
- 大規(guī)模點云
- 特征向量(FPFH、SHOT、Descriptors)
- 圖優(yōu)化結(jié)構(gòu)(因子圖、Jacobian Blocks)
右值引用 + 移動語義 就是解決上述性能瓶頸的關(guān)鍵。
二、值類別(value categories)——理解左右值很關(guān)鍵
C++11 之后共有 5 種值類別:
| 類別 | 描述 | 示例 |
|---|---|---|
| lvalue(左值) | 有名字,可取地址 | x、v[0] |
| xvalue(將亡值) | 即將被銷毀的對象,可以“偷資源” | std::move(x)、T&& 某些表達式 |
| prvalue(純右值) | 臨時值,無名稱 | T()、3、func() 返回臨時 |
| glvalue(泛左值) | lvalue + xvalue | 能代表 “對象的定位” |
| rvalue(右值) | xvalue + prvalue | 可移動但不能取地址 |
記住要點:
- 左值有身份,不必有值
- 右值有值,不必有身份
右值引用就是綁定 xvalue 與 prvalue 的一種引用。
關(guān)鍵:右值引用 T&& 能接受 xvalue/prvalue,但不接受 lvalue(除非用 std::move 或模板完美轉(zhuǎn)發(fā))。
三、右值引用與移動構(gòu)造/賦值(基本定義)
對類 T,推薦實現(xiàn)(rule of five):
struct T {
// 構(gòu)造/析構(gòu)
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();
};
示例 — 簡單資源類(動態(tài)數(shù)組)
#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;
}
};要點:
- 移動構(gòu)造/賦值竊取資源指針并把源對象置于安全的析構(gòu)態(tài)(通常設(shè)置為
nullptr、0)。 noexcept很重要 —— 它允許容器(比如std::vector<T>)在擴容時使用移動而不是拷貝(容器會在異常安全性不保證時退回拷貝策略)。
四、std::move、std::forward與完美轉(zhuǎn)發(fā)
std::move(x):把 lvalue 強制轉(zhuǎn)換成 xvalue(右值),告訴編譯器“可以竊取 x 的資源”。但它本身不移動;只是類型轉(zhuǎn)換。std::forward<T>(x):在模板中保留值類別(完美轉(zhuǎn)發(fā))。當(dāng)模板參數(shù)T是U&&的情況下,forward會在T為 lvalue-reference 時轉(zhuǎn)成 lvalue,否則轉(zhuǎn)成 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 的對象繼續(xù)使用可能導(dǎo)致未定義語義(安全但不可預(yù)測狀態(tài)),稱為 use-after-move。移動后的對象必須處于析構(gòu)與賦值安全狀態(tài),但其具體內(nèi)容不可依賴,除非類型指定了明確語義。
五、RVO / NRVO 與返回值優(yōu)化
在很多情況下函數(shù)返回臨時對象時會有拷貝或移動?,F(xiàn)代編譯器會做 (命名)返回值優(yōu)化(RVO / NRVO),避免額外拷貝/移動。C++17 更嚴格地把 prvalue 語義演進,使得通常不會觸發(fā)移動或拷貝(直接在調(diào)用者處構(gòu)造返回對象)。
示例:
Buffer make_buffer(size_t n){
Buffer tmp(n);
// ... fill ...
return tmp; // RVO: tmp 在調(diào)用處直接構(gòu)造
}
即便沒有 RVO,若 Buffer 有移動構(gòu)造,也會用移動構(gòu)造移動臨時對象(開銷?。?。
六、移動語義對標準容器與算法的影響
- 容器(
std::vector、std::deque等)會在重新分配時盡量移動元素(若T的移動構(gòu)造noexcept,容器使用移動;否則可能回退到拷貝)。因此,務(wù)必對能移動的大對象提供noexcept的移動構(gòu)造/賦值。 std::move_iterator、std::make_move_iterator可以把算法變?yōu)?ldquo;移動模式”:例如std::copy(std::make_move_iterator(first), std::make_move_iterator(last), dest)會把元素移動到dest。std::move_if_noexcept:在條件下選擇移動(如果移動拋異常則拷貝)。容器擴容等場景常使用這一策略。
七、移動語義和異常安全,為什么noexcept很重要
- 如果移動構(gòu)造有可能拋出異常,容器在重分配時可能無法保證強異常安全,會選擇回退到拷貝策略(更安全但更慢)。
- 因此 移動構(gòu)造/賦值應(yīng)盡可能聲明
noexcept。若必須在移動中做可能拋異常的操作,可以將那些操作放在拷貝 / swap 路徑中,或保證內(nèi)部操作不會拋異常。
推薦模式(move via swap):
T& operator=(T&& other) noexcept {
swap(*this, other);
return *this;
}但要確保 swap 本身 noexcept。
八、設(shè)計上的注意事項與常見陷阱
1) 移動后的對象狀態(tài)
移動后對象應(yīng)該處于有效但未指定內(nèi)容的狀態(tài),可安全析構(gòu)和賦值,但不能假設(shè)其值。文檔化被移動后對象的可用操作(建議僅能被賦值或析構(gòu))是一種好習(xí)慣。
2) 禁止移動操作(make class non-moveable)
如果類管理不可轉(zhuǎn)移資源(例如與 OS 綁定的唯一句柄,或禁止移動的語義),可以刪掉移動構(gòu)造:
T(T&&) = delete; T& operator=(T&&) = delete;
3) 輕量類型也可移動?是否必要?
對小 POD(如 int, double)移動沒有意義;移動語義主要針對“外部資源”。但實現(xiàn)移動構(gòu)造不會有壞處,只是多寫一點代碼。
4) 當(dāng)實現(xiàn)了自定義拷貝/移動時記住 Rule of Five
如果用戶定義了任一種:析構(gòu)(dtor)、拷貝構(gòu)造、拷貝賦值、移動構(gòu)造、移動賦值,應(yīng)考慮同時實現(xiàn)或禁用另外兩者以避免編譯器生成不合適的默認函數(shù)。
5) 完美轉(zhuǎn)發(fā)與重載決議
對于函數(shù)模板,重載 f(const T&) 與 f(T&&) 時注意:f(T&&) 對于左值不會匹配除非使用模板參數(shù)推導(dǎo)或 std::move。這常用于實現(xiàn) emplace_back 或工廠函數(shù)。
九、move-only 類型(典型示例:std::unique_ptr)
std::unique_ptr 是只可移動不可拷貝的典型例子。它利用移動語義保證資源唯一性。使用 unique_ptr 可以安全地把資源傳遞給函數(shù)或容器(容器會移動 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 起)。
十、模板與完美轉(zhuǎn)發(fā)的典型模式(emplace與工廠)
完美轉(zhuǎn)發(fā) 用于把參數(shù)原樣傳遞給構(gòu)造函數(shù)或函數(shù),避免不必要拷貝/移動:
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...) 使用完美轉(zhuǎn)發(fā)在目標存儲處直接構(gòu)造對象,避免臨時對象再移動/拷貝(相對于 push_back(T(args...)) 更高效)。
十一、性能分析
- 測量拷貝 VS 移動:在 Debug 模式下可能看不到差異(優(yōu)化關(guān)閉),要在 Release 編譯并測量真實應(yīng)用場景(例如大量點云向量移動)。
- profile container reallocation:如果類型的移動不是
noexcept,std::vector可能在擴容時拷貝元素造成意外開銷。檢查移動構(gòu)造是否noexcept。 - 避免不必要的
std::move:對返回局部變量的return std::move(local)會阻止 RVO,并且通常會產(chǎn)生多余的 move。不要在 return 語句中對返回值使用std::move(C++11/14 中反模式,C++17 prvalue 語義進一步緩解)。
十二、實戰(zhàn)建議
1) 表達式模板 / 延遲求值庫(Eigen、std::string 復(fù)制優(yōu)化)
大型數(shù)學(xué)庫(Eigen)使用表達式模板避免臨時對象。移動語義配合表達式模板可以極大減少分配和復(fù)制。
2) 委托內(nèi)存池 + move
當(dāng)對象使用自定義內(nèi)存池(固定內(nèi)存區(qū)域)時,移動構(gòu)造往往只是指針/偏移值的復(fù)制,性能幾乎是常數(shù)。
3) 多線程注意
在多線程場景中移動對象時要注意競爭:移動操作不是線程安全的;在移動前應(yīng)保證沒有其他線程同時訪問/修改該對象。
4)std::function與 move-only 可調(diào)用對象
std::function 在 C++11 中要求可拷貝目標;若要傳遞 unique_ptr 到回調(diào),可使用 std::move 包裝 lambda 捕獲: auto cb = [p = std::move(ptr)](){ ... };。C++17/20 中有更多靈活性(std::move_only_function 提案/實現(xiàn))。
十三、實戰(zhàn)注意要點
- 為管理資源的類實現(xiàn)移動構(gòu)造/移動賦值,且聲明
noexcept(如果安全)。 - 遵守 Rule of Five:若實現(xiàn)自定義 dtor/copy/move/assign,考慮實現(xiàn)完整五條或禁用不需要的。
- 對外部接口盡量接受
T&&或模板轉(zhuǎn)發(fā)參數(shù)以便移動(如push_back(T&&)/emplace_back)。 - 在庫/容器中使用
std::move_if_noexcept或合理選擇異常策略,以保證強異常安全。 - 避免在
return中使用std::move(local)(破壞 RVO)。 - 文檔化移動后對象的狀態(tài)與可調(diào)用操作(明確語義)。
- 在調(diào)試/性能測量時使用 Release 編譯并測量實際熱點(容器 reallocation、頻繁返回臨時等)。
- 在多線程中確保移動前后沒有并發(fā)訪問。
- 使用
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";
}在某些實現(xiàn)中,v1 在擴容時會將元素移動(更快),而 v2 因移動可能拋異常,會退回到拷貝(或觸發(fā)更復(fù)雜安全性處理),性能差異明顯。
十五、總結(jié)
- 移動語義 允許“竊取”臨時對象資源,極大減少高成本拷貝,提升性能。
- 右值引用
T&&與std::move/std::forward是實現(xiàn)移動語義與完美轉(zhuǎn)發(fā)的基礎(chǔ)工具。 - 實現(xiàn)移動時:務(wù)必保證移動后對象處于析構(gòu)/賦值安全狀態(tài),并盡量聲明
noexcept。 - 容器/算法 會基于可用的移動構(gòu)造采取不同策略(移動優(yōu)于拷貝),因此 move/
noexcept的設(shè)計會影響庫級別性能。 - 實踐:用
unique_ptr、emplace、make_move_iterator、完美轉(zhuǎn)發(fā)等現(xiàn)代 C++ 工具編寫高性能、異常安全的代碼。
到此這篇關(guān)于C++ 右值引用(rvalue references)與移動語義(move semantics)深度詳解的文章就介紹到這了,更多相關(guān)C++ 右值引用與移動語義內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
詳解C++模擬實現(xiàn)priority_queue(仿函數(shù))
本文主要介紹了關(guān)于C++中優(yōu)先級隊列的模擬實現(xiàn),以及仿函數(shù)的使用,優(yōu)先級隊列是一種容器適配器,其第一個元素總是最大的,仿函數(shù)本質(zhì)是一個類,重載了operator(),可改變比較邏輯,同時,文中還介紹了如何進行類的比較,如日期類或其指針的比較,以及庫中sort函數(shù)的使用方法2024-10-10
牛頓迭代法求多項式在1.5附近的值2*x的3次冪--4x平方+3*x-6=0的實現(xiàn)代碼
以下代碼是使用了牛頓迭代法求多項式在1.5附近的值 2*x的3次冪 - 4x的平方 + 3*x -6=0的實例。需要的朋友參考下吧2013-05-05
vscode ssh遠程連接服務(wù)器一直卡在下載 vscode server問題解決
在使用vscode使用ssh遠程連接服務(wù)器時,一直卡在下載"vscode 服務(wù)器"階段,本文主要介紹了vscode ssh遠程連接服務(wù)器一直卡在下載 vscode server問題解決,感興趣的可以了解一下2025-01-01
C語言?超詳細介紹與實現(xiàn)線性表中的無頭單向非循環(huán)鏈表
無頭單向非循環(huán)鏈表:結(jié)構(gòu)簡單,一般不會單獨用來存數(shù)據(jù)。實際中更多是作為其他數(shù)據(jù)結(jié)構(gòu)的子結(jié)構(gòu),如哈希桶、圖的鄰接表等等。另外這種結(jié)構(gòu)在筆試面試中出現(xiàn)很多2022-03-03

