義:從右值引用到移動(dòng)構(gòu)造的實(shí)戰(zhàn)指南)
有一回我?guī)蛨F(tuán)隊(duì)做代碼評(píng)審看到某個(gè)模塊的請(qǐng)求處理函數(shù)里寫著這樣的代碼把一個(gè)大對(duì)象塞進(jìn)vector之前前面加了個(gè)std::move后面又繼續(xù)對(duì)同一個(gè)對(duì)象做讀寫操作。結(jié)果線上出現(xiàn)偶發(fā)數(shù)據(jù)錯(cuò)亂排查了整整兩天最后發(fā)現(xiàn)是 move 之后的源對(duì)象狀態(tài)已被破壞代碼卻還拿著它當(dāng)正常對(duì)象用。這件事讓我對(duì) C Move 語(yǔ)義的使用要點(diǎn)格外上心。Move 語(yǔ)義是 C11 引入的核心能力它解決兩類問題一個(gè)是拷貝開銷太高的性能問題另一個(gè)是“只能移動(dòng)、不能拷貝”的所有權(quán)轉(zhuǎn)移問題。但說句實(shí)話很多項(xiàng)目里 move 不是沒人用而是要么不敢用要么無腦用真正用對(duì)的人不多。這篇文章想把 Move 的底層原理、正確的打開方式、容易翻車的坑一次講透適合正在用 C11/14/17/20 做開發(fā)的同事也適合剛接觸現(xiàn)代 C 的初學(xué)者。1. Move語(yǔ)義解決的真實(shí)痛點(diǎn)一次深拷貝引發(fā)的性能與所有權(quán)焦慮1.1 一個(gè)我實(shí)際遇到的拷貝爆炸場(chǎng)景先看一段很常見的代碼。假設(shè)有個(gè)函數(shù)要解析一坨配置文本返回解析后的行列表std::vectorstd::string parseConfig(const std::string raw) { std::vectorstd::string lines; // 模擬解析過程把 raw 按行拆分 for (size_t pos 0; pos raw.size(); ) { size_t next raw.find(\n, pos); if (next std::string::npos) { lines.push_back(raw.substr(pos)); break; } lines.push_back(raw.substr(pos, next - pos)); pos next 1; } return lines; }在 C11 之前如果編譯器不做返回值優(yōu)化RVO這個(gè)函數(shù)的主調(diào)用方拿到返回值時(shí)vector和里面每個(gè)string都要整體深拷貝一份。配置文件哪怕只有幾百 KB一次函數(shù)調(diào)用都會(huì)帶來幾十次甚至上千次堆內(nèi)存分配和字節(jié)復(fù)制。這在舊代碼里是真實(shí)存在的性能黑洞。C11 引入移動(dòng)語(yǔ)義之后同樣這段代碼return lines;時(shí)編譯器會(huì)優(yōu)先把lines內(nèi)部的堆內(nèi)存“偷”走而不是重新復(fù)制一份。整套操作只需要修改幾個(gè)指針和長(zhǎng)度字段源對(duì)象則被置為一個(gè)空殼狀態(tài)。這是 Move 語(yǔ)義最典型的收益場(chǎng)景。1.2 移動(dòng)的本質(zhì)轉(zhuǎn)移資源而不是復(fù)制資源很多材料喜歡用“書架搬書”來類比我換個(gè)更貼近直覺的說法拷貝是你要一本書就跑去復(fù)印一整本移動(dòng)是你知道這本馬上要被銷毀或丟棄直接把書拿過來省的再抄一遍。在 C 實(shí)現(xiàn)層面一個(gè)有堆資源的類型大概長(zhǎng)這樣class Buffer { public: explicit Buffer(std::size_t size) : data_(new char[size]), size_(size) {} ~Buffer() { delete[] data_; } // 拷貝構(gòu)造復(fù)制資源 Buffer(const Buffer other) : data_(new char[other.size_]), size_(other.size_) { std::memcpy(data_, other.data_, size_); } // 移動(dòng)構(gòu)造轉(zhuǎn)移資源 Buffer(Buffer other) noexcept : data_(other.data_), size_(other.size_) { other.data_ nullptr; other.size_ 0; } private: char* data_; std::size_t size_; };拷貝構(gòu)造做了兩件事分配新內(nèi)存塊然后把舊數(shù)據(jù)逐字節(jié)復(fù)制過去。移動(dòng)構(gòu)造什么都沒復(fù)制只是把源對(duì)象的data_指針拿過來再把源對(duì)象的指針置空。這里有個(gè)關(guān)鍵認(rèn)知移動(dòng)不是“更快的拷貝”而是“完全不拷貝”。如果你把移動(dòng)理解為一種“性能更好的復(fù)制”那很多決策都會(huì)做錯(cuò)。2. 地基先打好右值引用、std::move與移動(dòng)構(gòu)造函數(shù)的三角關(guān)系2.1 右值引用T到底綁定的是什么要理解 Move就必須先理解它背后的類型機(jī)制。C 里表達(dá)式分成兩大類左值和右值。左值有名字、可以被取地址、會(huì)在后續(xù)代碼里繼續(xù)存活右值是沒有持久身份的臨時(shí)對(duì)象表達(dá)式的生命周期通常在這個(gè)語(yǔ)句結(jié)束時(shí)就結(jié)束了。比如int x 42;里的x是左值42是右值。std::string(hello)這個(gè)臨時(shí)對(duì)象也是右值。右值引用T是 C11 新增的引用類型它只能綁定到右值上。為什么 move 相關(guān)的函數(shù)普遍用T作為參數(shù)因?yàn)榫幾g器在暗示一件事這個(gè)參數(shù)是即將銷毀的臨時(shí)對(duì)象你可以不客氣地把它身上的資源榨干反正沒人會(huì)在乎。看幾個(gè)綁定關(guān)系void func(int lref) {} // 只能傳左值 void func(int rref) {} // 只能傳右值 int x 0; func(x); // 匹配 int func(x 1); // 匹配 int因?yàn)?x1 的結(jié)果是右值 func(std::move(x)); // 也可以匹配 int左值引用T不能綁定右值右值引用T不能綁定左值。有一條稍微繞的規(guī)則一個(gè)具名的右值引用變量它在表達(dá)式里仍然是左值。比如void store(std::string s) { // s 這個(gè)名字有地址、有名字所以在函數(shù)體里 s 是左值 internalStore(std::move(s)); // 必須再 move 一次才能真正移動(dòng) }這個(gè)細(xì)節(jié)是大量 bug 的來源繼續(xù)往下看。2.2 std::move真的“移動(dòng)”了什么沒有std::move這個(gè)命名誤導(dǎo)了無數(shù)人。它根本不移動(dòng)任何數(shù)據(jù)它的全部作用只是一個(gè)類型轉(zhuǎn)換。標(biāo)準(zhǔn)庫(kù)里它的典型實(shí)現(xiàn)是template typename T constexpr typename std::remove_referenceT::type move(T t) noexcept { return static_casttypename std::remove_referenceT::type(t); }看到了嗎就只有一行static_cast。它把傳入的參數(shù)顯式轉(zhuǎn)換成一個(gè)右值引用讓編譯器在做重載決議的時(shí)候能選中移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符。真正的搬運(yùn)工作發(fā)生在移動(dòng)構(gòu)造函數(shù)、移動(dòng)賦值運(yùn)算符的代碼里而不是std::move本身。為了證明這件事我經(jīng)常給新人做一個(gè)實(shí)驗(yàn)int x 42; std::cout x \n; // 輸出 42 std::move(x); std::cout x \n; // 仍然輸出 42std::move(x)對(duì)int只是一個(gè)類型轉(zhuǎn)換根本沒有“移動(dòng)”走任何東西。因?yàn)閕nt沒有移動(dòng)構(gòu)造函數(shù)它的“移動(dòng)”本質(zhì)上還是拷貝。這個(gè)實(shí)驗(yàn)雖然簡(jiǎn)單但能把“move 只是選路不是搬運(yùn)”這個(gè)概念刻進(jìn)腦子里。理解了這一點(diǎn)再看下面這個(gè)經(jīng)典代碼就不會(huì)困惑了void logAndStore(std::string s) { m_cache.store(std::move(s)); // 沒有這層 std::move下面就是一次拷貝 }logAndStore的參數(shù)是std::string s但s本身是具名變量是左值。把它傳給store時(shí)如果直接寫store(s)編譯器看到的是左值會(huì)選擇拷貝構(gòu)造。必須在函數(shù)內(nèi)部再包一層std::move(s)把s重新轉(zhuǎn)換成右值引用store才會(huì)走移動(dòng)路徑。2.3 手寫移動(dòng)構(gòu)造函數(shù)時(shí)三個(gè)必須遵守的規(guī)范在自定義類型里寫移動(dòng)構(gòu)造和移動(dòng)賦值時(shí)有三條規(guī)矩是我的硬性要求。第一條必須標(biāo)noexcept。這一點(diǎn)直接關(guān)系到標(biāo)準(zhǔn)庫(kù)容器的性能行為。std::vector擴(kuò)容時(shí)元素需要從舊內(nèi)存搬到新內(nèi)存。如果移動(dòng)構(gòu)造函數(shù)可能拋異常vector無法保證異常安全——前面的元素已經(jīng)移走了、后面的還沒移一旦中途拋異常整個(gè)容器會(huì)處于斷裂狀態(tài)。所以標(biāo)準(zhǔn)庫(kù)普遍使用std::move_if_noexcept來抉擇移動(dòng)構(gòu)造是noexcept就用移動(dòng)否則退回拷貝構(gòu)造。很多人寫了移動(dòng)構(gòu)造函數(shù)卻發(fā)現(xiàn)擴(kuò)容時(shí)還是慢吞吞地拷貝排查半天原因就是漏了noexcept。第二條必須把源對(duì)象置為可安全析構(gòu)的狀態(tài)。移動(dòng)構(gòu)造內(nèi)部拿走源對(duì)象的data_指針后如果不把other.data_置空源對(duì)象析構(gòu)時(shí)會(huì)delete掉已經(jīng)被轉(zhuǎn)移走的那塊內(nèi)存目標(biāo)對(duì)象就會(huì)拿到一個(gè)懸垂指針。這相當(dāng)于你把鑰匙交給了別人自己手里還留著備份別人已經(jīng)換鎖進(jìn)門了你還在拿舊鑰匙去開同一扇門。第三條移動(dòng)賦值要處理自移動(dòng)。a std::move(a)這種寫法看起來極端但在泛型代碼、算法重排、并發(fā)回調(diào)里完全可能出現(xiàn)。如果移動(dòng)賦值沒做自移動(dòng)檢查先把自己的資源釋放掉再執(zhí)行賦值等于原地自殺。拷貝賦值里大家習(xí)慣寫if (this ! other)移動(dòng)賦值同樣需要。常規(guī)寫法是Buffer operator(Buffer other) noexcept { if (this ! other) { delete[] data_; data_ other.data_; size_ other.size_; other.data_ nullptr; other.size_ 0; } return *this; }但我實(shí)際寫代碼時(shí)更喜歡比較穩(wěn)妥的 swap 風(fēng)格Buffer operator(Buffer other) noexcept { std::swap(data_, other.data_); std::swap(size_, other.size_); return *this; }這種寫法不用檢查自移動(dòng)因?yàn)閟wap兩個(gè)相同對(duì)象等于什么都不發(fā)生。而且異常安全更好如果移動(dòng)過程中出問題源對(duì)象還抱著舊資源析構(gòu)時(shí)會(huì)釋放不會(huì)泄漏。唯一的缺點(diǎn)是它對(duì)讀者沒那么直觀最好配一行注釋解釋為什么用 swap。3. 什么時(shí)候該Move什么時(shí)候Move了也白搭3.1 收益最明顯的場(chǎng)景容器擴(kuò)容、按值傳參與重排我會(huì)給團(tuán)隊(duì)定的實(shí)戰(zhàn)準(zhǔn)則是先看對(duì)象內(nèi)部有沒有“外部資源”再看后續(xù)流程里有沒有深拷貝風(fēng)險(xiǎn)。收益最大的場(chǎng)景有這么幾類。第一類是按值傳參后把參數(shù)存進(jìn)成員或容器。最常見的慣用法是構(gòu)造函數(shù)的成員初始化列表class Task { public: explicit Task(std::string name) : name_(std::move(name)) {} private: std::string name_; };這里如果寫成name_(name)就是一次深拷貝因?yàn)閚ame是局部左值。寫成name_(std::move(name))之后name_直接把name手里的字符數(shù)組接管了。代價(jià)趨近于零。這個(gè)模式叫“對(duì)拷碰掉”在需要頻繁構(gòu)造對(duì)象的代碼里收益非常明顯。第二類是容器擴(kuò)容和元素搬運(yùn)。std::vector的push_back(std::move(x))、std::sort內(nèi)部對(duì)元素的重排、std::remove_if的壓縮都依賴移動(dòng)語(yǔ)義。尤其是元素很多、拷貝很重的容器移動(dòng)在這里的意義不是“優(yōu)化一點(diǎn)”而是“數(shù)量級(jí)的差別”。第三類是swap 操作?,F(xiàn)代 C 里std::swap通常實(shí)現(xiàn)為三次移動(dòng)template typename T void swap(T a, T b) { T tmp(std::move(a)); a std::move(b); b std::move(tmp); }對(duì)管理堆內(nèi)存的類型這比傳統(tǒng)的三次深拷貝便宜太多。這也是為什么有移動(dòng)語(yǔ)義之后很多算法可以直接受益不需要改調(diào)用代碼。3.2 返回值上加std::move是典型的反優(yōu)化這是我看到頻率最高的誤用之一函數(shù)返回局部對(duì)象時(shí)有人習(xí)慣寫上return std::move(local);覺得“我在主動(dòng)移動(dòng)肯定更快”。實(shí)際上這往往是反優(yōu)化。關(guān)鍵在返回值優(yōu)化RVOReturn Value Optimization。C17 標(biāo)準(zhǔn)對(duì)“返回臨時(shí)對(duì)象”這種純右值場(chǎng)景已經(jīng)強(qiáng)制要求省略拷貝對(duì)“返回具名局部變量”的具名返回值優(yōu)化NRVO則仍然允許但不強(qiáng)制。主流編譯器在優(yōu)化級(jí)別下通常都會(huì)做 NRVO把局部變量直接構(gòu)造到調(diào)用方的結(jié)果對(duì)象里連移動(dòng)都省了。一旦你寫了return std::move(local);表達(dá)式變成了一個(gè)右值xvalue它不再是一個(gè)“具名的局部對(duì)象”編譯器按 NRVO 規(guī)則無法直接省略只能退而求其次走移動(dòng)構(gòu)造。有些編譯器優(yōu)化能力強(qiáng)最后結(jié)果還湊合但這是把自己的性能交給編譯器的臉色完全沒有必要。正確寫法就是std::vectorstd::string buildConfig() { std::vectorstd::string result; // ... return result; // 直接返回讓編譯器自己選擇 RVO 或 IMO }我給團(tuán)隊(duì)立的規(guī)矩是返回值絕不寫std::move直接返回局部對(duì)象即可。3.3 move-only類型與引用計(jì)數(shù)的移動(dòng)語(yǔ)義差異有一類類型從出生就注定不能拷貝比如std::unique_ptr、std::thread、std::fstream。它們的拷貝構(gòu)造函數(shù)被顯式刪除想轉(zhuǎn)移所有權(quán)只能靠移動(dòng)。在這個(gè)場(chǎng)景里Move 語(yǔ)義不是“性能優(yōu)化”而是語(yǔ)言層面的所有權(quán)表達(dá)std::unique_ptrConfig loadConfig() { // 返回時(shí)編譯器自動(dòng)移動(dòng)不需要寫 std::move return std::make_uniqueConfig(); } void process(std::unique_ptrConfig cfg) { // ... } auto cfg loadConfig(); // process(cfg); // 錯(cuò)誤unique_ptr 不能拷貝 process(std::move(cfg)); // 顯式轉(zhuǎn)移所有權(quán) // 此時(shí) cfg 已經(jīng)是 nullptr不能再使用這個(gè)例子同時(shí)也展示了 moved-from 對(duì)象的經(jīng)典狀態(tài)轉(zhuǎn)移后cfg變成空指針寫入、析構(gòu)都安全但你不能再把它當(dāng)正常對(duì)象用。std::shared_ptr比較特殊。它的拷貝成本主要在兩個(gè)原子操作上——控制塊引用計(jì)數(shù)的遞增和遞減。所以移動(dòng)shared_ptr確實(shí)能省掉原子操作的開銷在性能敏感路徑上有收益。但shared_ptr本身還包含一個(gè)堆上的控制塊結(jié)構(gòu)比unique_ptr重移動(dòng)收益沒有 move-only 類型那么極致。我的建議是如果場(chǎng)景允許優(yōu)先用unique_ptr表達(dá)獨(dú)有所有權(quán)別動(dòng)不動(dòng)就shared_ptr。4. 那些容易讓程序“悄悄壞掉”的Move陷阱4.1 moved-from對(duì)象不是“空對(duì)象”有效但未指定C 標(biāo)準(zhǔn)對(duì)“移動(dòng)后的源對(duì)象”有一個(gè)非常微妙的表述有效但未指定valid but unspecified。意思是它能繼續(xù)被賦值、能安全析構(gòu)但你不能假設(shè)它的具體內(nèi)容是什么。很多開發(fā)者以為std::move之后源對(duì)象一定是空的。unique_ptr確實(shí)如此std::vector、std::string的主流實(shí)現(xiàn)也基本會(huì)把源對(duì)象置空。但標(biāo)準(zhǔn)沒有強(qiáng)制要求這一點(diǎn)。對(duì)于自定義類型移動(dòng)構(gòu)造函數(shù)里把源對(duì)象設(shè)置成什么狀態(tài)完全由你決定。只要目標(biāo)對(duì)象的狀態(tài)合法、源對(duì)象仍可安全析構(gòu)移動(dòng)就是符合規(guī)范的。我在項(xiàng)目里見過的真實(shí)事故是模塊 A 把一個(gè)大的std::string移動(dòng)塞進(jìn)隊(duì)列模塊 B 收到后使用隊(duì)列內(nèi)容模塊 A 這邊還繼續(xù)讀取源string做日志拼接。結(jié)果時(shí)好時(shí)壞因?yàn)槟承?shí)現(xiàn)里源對(duì)象被置空某些路徑里沒有。跨平臺(tái)后行為還不一樣排錯(cuò)排到懷疑人生。安全做法很簡(jiǎn)單move 之后如果還想用這個(gè)對(duì)象唯一可靠的方式是立刻給它賦予一個(gè)明確的新值。std::string tmp std::move(m_cache); // 緩存被掏空 m_cache.clear(); // 顯式重置鎖定語(yǔ)義4.2 自移動(dòng)賦值很多人不知道的未定義行為a std::move(a)看起來荒謬但在泛型算法、信號(hào)處理、資源回收這些代碼路徑里你根本控制不住傳入的到底是哪個(gè)對(duì)象。如果自移動(dòng)賦值沒有正確處理結(jié)果可能是災(zāi)難性的。有一次我調(diào)試一個(gè)模塊發(fā)現(xiàn)一個(gè)緩存對(duì)象的內(nèi)部指針變成野指針數(shù)據(jù)被寫壞了。打斷點(diǎn)看的時(shí)候發(fā)現(xiàn)某個(gè)重試邏輯在異常路徑上執(zhí)行了self std::move(self)而移動(dòng)賦值運(yùn)算符是先delete[] data_再data_ other.data_——自移動(dòng)時(shí)other就是this等于把自己釋放掉又把自己地址塞給自己直接崩。標(biāo)準(zhǔn)庫(kù)容器在 C20 后明確要求自移動(dòng)賦值的結(jié)果是“有效但未指定”不會(huì)讓你懸垂但這是對(duì)標(biāo)準(zhǔn)庫(kù)的要求不是對(duì)你自定義類型的保證。所以移動(dòng)賦值運(yùn)算符里保留自移動(dòng)檢查或使用 swap 風(fēng)格不是可選項(xiàng)是必須項(xiàng)。4.3 成員移動(dòng)與析構(gòu)的糾纏懸垂資源怎么避免移動(dòng)構(gòu)造的正確性不光取決于你是否把源對(duì)象置空還取決于源對(duì)象的析構(gòu)函數(shù)。一個(gè)很常見的設(shè)計(jì)是“工廠方法從對(duì)象里取走某個(gè)資源”struct Outer { std::unique_ptrInner inner; std::unique_ptrInner takeInner() { return std::move(inner); // 把所有權(quán)交出去 } };takeInner()是安全的因?yàn)閡nique_ptr移動(dòng)后變成nullptrOuter析構(gòu)時(shí)不會(huì)有任何問題。但如果換成自定義類型就不一定了。自定義類型的移動(dòng)構(gòu)造函數(shù)如果沒有正確置空源對(duì)象源對(duì)象析構(gòu)時(shí)會(huì)釋放已經(jīng)被目標(biāo)對(duì)象持有的資源導(dǎo)致 double free。這種問題的可怕之處在于它往往不立即崩潰而是過一段時(shí)間在內(nèi)存池里爆炸很難復(fù)現(xiàn)。寫移動(dòng)構(gòu)造函數(shù)時(shí)我習(xí)慣在實(shí)現(xiàn)里加一條注釋提醒自己逐成員檢查“這個(gè)成員移到目標(biāo)后源對(duì)象析構(gòu)時(shí)會(huì)不會(huì)碰到同一塊資源不會(huì)——因?yàn)槲乙呀?jīng)置空了?!边@個(gè)逐成員的審視比任何靜態(tài)檢查工具都靠譜。4.4 完美轉(zhuǎn)發(fā)中的std::move是“越界”這里該用std::forward在模板里T并不一定是右值引用。它是轉(zhuǎn)發(fā)引用forwarding reference也叫萬能引用。這導(dǎo)致很多人在寫模板時(shí)把std::move和std::forward混用釀成大錯(cuò)。template typename T void wrapper(T arg) { // 注意arg 是一個(gè)具名變量在表達(dá)式里永遠(yuǎn)是左值 target(std::move(arg)); // 問題所在把左值也強(qiáng)制轉(zhuǎn)成右值 }如果調(diào)用方傳入一個(gè)左值objT會(huì)被推導(dǎo)為Targ是左值引用調(diào)用方本意是希望target也按左值處理可以繼續(xù)持有并拷貝使用。但wrapper內(nèi)部寫死std::move(arg)等于強(qiáng)制按右值處理直接掏空了調(diào)用方的對(duì)象。正確姿勢(shì)是使用std::forwardtemplate typename T void wrapper(T arg) { target(std::forwardT(arg)); }std::forwardT(arg)會(huì)根據(jù)T的推導(dǎo)結(jié)果決定返回左值引用還是右值引用調(diào)用方傳左值時(shí)返回左值引用傳右值時(shí)返回右值引用。它的作用簡(jiǎn)單說就是“保持原樣轉(zhuǎn)發(fā)”和std::move的“無條件變成右值”有本質(zhì)區(qū)別。還有一個(gè)新手的經(jīng)典錯(cuò)誤在移動(dòng)構(gòu)造和移動(dòng)賦值參數(shù)上寫const T。const修飾的右值引用可以接受右值但函數(shù)體內(nèi)無法修改對(duì)象也就無法把源對(duì)象的指針置空——那等于移動(dòng)了個(gè)寂寞。寫const Buffer還不如直接寫const Buffer至少語(yǔ)義是清楚的。5. 用實(shí)測(cè)數(shù)據(jù)說話Move的成本與收益邊界5.1 一組簡(jiǎn)單基準(zhǔn)string、vector在不同大小下的移動(dòng)比拷貝省多少說再多原理不如讓數(shù)據(jù)說話。我在某內(nèi)部項(xiàng)目里寫過一組很簡(jiǎn)單的基準(zhǔn)思路是反復(fù)構(gòu)造一個(gè)大對(duì)象再分別用拷貝和移動(dòng)把它轉(zhuǎn)移給另一個(gè)對(duì)象記錄總耗時(shí)。大致長(zhǎng)這樣#include chrono #include iostream #include string #include vector int main() { int times 100000; auto t0 std::chrono::steady_clock::now(); for (int i 0; i times; i) { std::string src(100000, x); std::string dst(src); } auto t1 std::chrono::steady_clock::now(); for (int i 0; i times; i) { std::string src(100000, x); std::string dst(std::move(src)); } auto t2 std::chrono::steady_clock::now(); auto copyMs std::chrono::duration_caststd::chrono::milliseconds(t1 - t0).count(); auto moveMs std::chrono::duration_caststd::chrono::milliseconds(t2 - t1).count(); std::cout copy: copyMs ms, move: moveMs ms\n; }結(jié)果雖然在不同機(jī)器上有差異數(shù)量級(jí)關(guān)系是穩(wěn)定的。一百字節(jié)以上的大std::string、幾千個(gè)元素以上的大std::vector移動(dòng)耗時(shí)只有拷貝的幾十分之一。而當(dāng)一個(gè)std::string短到觸發(fā)小字符串優(yōu)化SSO短字符串直接存在對(duì)象內(nèi)部時(shí)雙方耗時(shí)幾乎一樣因?yàn)橐苿?dòng)也要搬運(yùn)對(duì)象體內(nèi)那一小塊數(shù)據(jù)。表格大概是這樣類型/大小拷貝耗時(shí)對(duì)比移動(dòng)耗時(shí)對(duì)比說明長(zhǎng) std::string約 100KB極高極低移動(dòng)省掉整塊堆內(nèi)存分配和復(fù)制短 std::stringSSO 內(nèi)低低數(shù)據(jù)在對(duì)象體內(nèi)移動(dòng)約等于拷貝std::vector 10 個(gè)元素低低堆指針搬運(yùn)收益不顯著std::vector 10 萬個(gè)元素極高極低典型的移動(dòng)收益場(chǎng)景自定義 Buffer大塊 char[]極高極低指針交換開銷幾乎為零5.2 結(jié)合實(shí)測(cè)給出的選型建議別為了Move而Move從這些數(shù)據(jù)能得出一條非常清晰的結(jié)論Move 的收益與對(duì)象內(nèi)部是否有外部資源以及外部資源的大小成正比。沒有外部資源、全靠對(duì)象自身存儲(chǔ)的類型POD、小型struct、SSO 短字符串、std::array移動(dòng)和拷貝的差距小到可以忽略寫std::move只是往代碼里注入噪聲。我實(shí)際做項(xiàng)目時(shí)的流程是先看類型管理堆內(nèi)存、文件句柄、套接字、鎖等資源的類型優(yōu)先考慮移動(dòng)POD 和小對(duì)象不值得為 move 折騰。再看場(chǎng)景函數(shù)返回局部對(duì)象、按值傳參后存儲(chǔ)、元素大量進(jìn)出容器、算法重排這些場(chǎng)景 Move 收益明確。對(duì)象生命周期馬上結(jié)束、不需要再存新值Move 沒意義。最后看可讀性std::move是給讀代碼的人一個(gè)信號(hào)——這個(gè)對(duì)象接下來可能被掏空。如果收益不明確加上它只會(huì)增加心智負(fù)擔(dān)。我最后再分享一個(gè)個(gè)人習(xí)慣寫自定義類型時(shí)先想清楚它的資源所有權(quán)模型然后把它設(shè)計(jì)成 move-only 或至少顯式刪除拷貝構(gòu)造而不是默認(rèn)讓它既能拷貝又能移動(dòng)。Move 語(yǔ)義最值錢的地方不是“更快地復(fù)制”而是讓“資源只能有一個(gè)擁有者”這件事變成編譯器強(qiáng)制檢查的規(guī)則。把這一點(diǎn)用好了比任何微優(yōu)化都更能讓代碼活得久。