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

