全面掌握 C++ && 的兩種模式及其應(yīng)用場(chǎng)景
今天,來(lái)扒一扒 C++ 里容易讓人理解混淆的 && 符號(hào),特別是它在“萬(wàn)能引用”和“右值引用”這兩個(gè)身份間反復(fù)橫跳的騷操作。
第一:右值引用 - "一次性"道具的專(zhuān)屬接收器
在 C++11 這個(gè)偉大的版本問(wèn)世之前,我們 C++ 程序員過(guò)著相對(duì)"樸素"的生活。
對(duì)象要么是"有名有姓"的左值(Lvalue),比如 int a = 10; 里的 a,你可以反復(fù)用它的名字找到它,給它賦值,取它的地址,就像你家養(yǎng)的那只可以擼可以喂、隨時(shí)能找到的貓。
要么就是曇花一現(xiàn)的右值(Rvalue),比如 10、a + b 的結(jié)果、函數(shù)返回的臨時(shí)對(duì)象 getString()。它們就像你在路邊撿到的、用完就可能消失的優(yōu)惠券,或者外賣(mài)送的一次性筷子,用完就扔,通常沒(méi)有名字,也不能(或者不應(yīng)該)對(duì)它們進(jìn)行修改。
拷貝這些"一次性用品"往往是浪費(fèi)的。比如你寫(xiě) std::string s = getString();,如果 getString() 返回一個(gè)臨時(shí)的 std::string 對(duì)象(右值),老版本的 C++ 會(huì)傻乎乎地把這個(gè)臨時(shí)對(duì)象里的數(shù)據(jù)(比如一大段文字)完完整整地復(fù)制一份到新的 s 對(duì)象里,然后那個(gè)臨時(shí)對(duì)象就被銷(xiāo)毀了。這就像你點(diǎn)外賣(mài),人家送來(lái)一份用精美一次性餐盒裝的飯,你非得把它小心翼翼地倒進(jìn)你自己的碗里,然后把那個(gè)還能用的餐盒扔掉... 何必呢?
于是,C++11 帶來(lái)了右值引用,語(yǔ)法就是 類(lèi)型&&。它的核心使命只有一個(gè):綁定到右值!
void process_disposable(std::string&& disposable_cup){
// 這里的 disposable_cup 明確表示:我只接收那些“一次性”的 string!
std::cout << "Processing the disposable cup's content: " << disposable_cup << std::endl;
// 重點(diǎn)來(lái)了:我可以“偷”走它的資源!
std::string my_permanent_mug = std::move(disposable_cup); // 資源轉(zhuǎn)移,杯子空了
std::cout << "Content moved to my mug: " << my_permanent_mug << std::endl;
// 注意:disposable_cup 現(xiàn)在可能為空了,不能再依賴(lài)它的內(nèi)容了
}
intmain(){
std::string permanent_bottle = "Water";
// process_disposable(permanent_bottle); // 編譯錯(cuò)誤!人家不要你的“永久水瓶”(左值)
process_disposable("Juice"); // OK!"Juice" 是個(gè)臨時(shí)字符串(右值)
process_disposable(std::string("Milk")); // OK!std::string("Milk") 創(chuàng)建臨時(shí)對(duì)象(右值)
std::string another_bottle = "Soda";
// 如果你非要把你的永久水瓶當(dāng)一次性的給,需要顯式“打包”
process_disposable(std::move(another_bottle)); // OK!std::move 把它偽裝成右值
// 但要小心,another_bottle 的內(nèi)容可能被“偷”走了!
return0;
}
生活案例:右值引用就像是"二手閑置物品接收點(diǎn)"
想象一下,你家小區(qū)門(mén)口有個(gè)牌子寫(xiě)著:“閑置物品(即將丟棄)接收點(diǎn),聯(lián)系人:張三 &&”。
- 規(guī)則:這個(gè)接收點(diǎn)(張三&&)只接收那些你明確表示“我不要了,準(zhǔn)備扔了”的東西(右值)。比如你剛喝完的一次性飲料瓶、過(guò)期的雜志、穿不了的舊衣服。
- 好處:張三(右值引用)拿到這些東西后,可以“廢物利用”,比如把瓶子拿去賣(mài)錢(qián),把雜志內(nèi)容剪下來(lái)做手工,把舊衣服拆了做抹布(對(duì)應(yīng) C++ 的移動(dòng)語(yǔ)義 ,轉(zhuǎn)移資源而不是拷貝)。他知道這些東西的原主人不打算再要了,所以可以大膽地“破壞性”使用。
- 限制:你不能把你家祖?zhèn)鞯?、還在用的電視機(jī)(左值 my_tv)直接搬過(guò)去給張三,他會(huì)拒收,說(shuō):“嘿!這玩意兒你還用呢,我不能收!”. 除非你鄭重聲明:“這電視我確實(shí)不要了!給你了!”,相當(dāng)于你對(duì)電視機(jī)用了 std::move(my_tv),把它“標(biāo)記”為可以被接收的狀態(tài)。但一旦你這么做了,就別指望回家還能看這臺(tái)電視了,它的“靈魂”(資源)可能已經(jīng)被張三搬走了。
總結(jié)一下右值引用:
- 語(yǔ)法:類(lèi)型&& (在類(lèi)型 不是 模板參數(shù)推導(dǎo)上下文,或者 auto&& 推導(dǎo)上下文時(shí))
- 作用:專(zhuān)門(mén)綁定到右值。
- 目的:實(shí)現(xiàn)移動(dòng)語(yǔ)義,避免不必要的拷貝,提升性能。就像那個(gè)只收閑置品的張三,高效利用資源。
第二:萬(wàn)能引用 - "百變星君"的身份魔法
再說(shuō)到萬(wàn)能引用!它是 C++ 界的“百變星君”,它也用 && 符號(hào),但玩法完全不同!
萬(wàn)能引用,由 Scott Meyers 大神提出,雖然現(xiàn)在官方和很多開(kāi)發(fā)者更傾向于叫它 轉(zhuǎn)發(fā)引用,但“萬(wàn)能引用”這個(gè)名字實(shí)在太形象了,我們先用著,后面再?gòu)?qiáng)調(diào)它的核心使命是“轉(zhuǎn)發(fā)”。
萬(wàn)能引用只在特定的上下文中出現(xiàn),滿(mǎn)足以下兩個(gè)條件時(shí),T&& 才不是右值引用,而是萬(wàn)能引用:
1. 發(fā)生在模板類(lèi)型推導(dǎo)中:函數(shù)模板的參數(shù)類(lèi)型是 T&&,其中 T 是需要推導(dǎo)的模板參數(shù)。
template<typename T>
void magic_box(T&& item) { // <--- 這里的 T&& 就是萬(wàn)能引用!
// ... 魔法操作 ...
}
2. 發(fā)生在 auto類(lèi)型推導(dǎo)中:變量聲明使用 auto&&。
auto&& magic_variable = some_expression; // <--- 這里的 auto&& 也是萬(wàn)能引用!
關(guān)鍵區(qū)別:看到?jīng)]?類(lèi)型推導(dǎo)!這就是區(qū)分它是“專(zhuān)一的右值引用”還是“百變的萬(wàn)能引用”的唯一標(biāo)準(zhǔn)!
1、如果 && 所在的類(lèi)型涉及到編譯器的類(lèi)型推導(dǎo)(typename T 或 auto),那它就是萬(wàn)能引用;
2、如果類(lèi)型是寫(xiě)死的(比如 std::string&&),那就是右值引用。
那么,“萬(wàn)能”體現(xiàn)在哪里呢?
萬(wàn)能引用之所以“萬(wàn)能”,是因?yàn)樗瓤梢越壎ǖ阶笾担部梢越壎ǖ接抑?!?jiǎn)直是通吃!
- 當(dāng)你傳遞一個(gè)左值給萬(wàn)能引用時(shí),模板參數(shù) T 會(huì)被推導(dǎo)為左值引用類(lèi)型(例如 int&),然后根據(jù) C++ 的引用折疊規(guī)則,T&& (即 int& &&)會(huì)折疊成 int&(左值引用)。
- 當(dāng)你傳遞一個(gè)右值給萬(wàn)能引用時(shí),模板參數(shù) T 會(huì)被推導(dǎo)為普通類(lèi)型(例如 int),T&& (即 int&&)就保持為 int&&(右值引用)。
- 記?。和茖?dǎo)的結(jié)果只有兩個(gè):左值引用或者普通類(lèi)型,沒(méi)有右值引用
轉(zhuǎn)發(fā)引用這套特殊的類(lèi)型推導(dǎo)規(guī)則總結(jié):
- 規(guī)則 1:如果傳遞給 T&& 的實(shí)參是一個(gè)左值 (Lvalue) ,類(lèi)型為 U,那么 T 會(huì)被推導(dǎo)為 U& (左值引用類(lèi)型)。
- 規(guī)則 2:如果傳遞給 T&& 的實(shí)參是一個(gè)右值 (Rvalue) ,類(lèi)型為 U,那么 T 會(huì)被推導(dǎo)為 U (原始非引用類(lèi)型)。
引用折疊規(guī)則小抄(記住這個(gè),你就掌握了萬(wàn)能引用的核心秘密):
- T& & -> T& (左引用 的 左引用 還是 左引用)
- T& && -> T& (左引用 的 右引用 變成 左引用)
- T&& & -> T& (右引用 的 左引用 變成 左引用)
- T&& && -> T&& (右引用 的 右引用 還是 右引用)
簡(jiǎn)單記:
1、只要有 &(左值引用)參與折疊,結(jié)果就是 &(左值引用)。
2、只有 && 和 &&碰頭,結(jié)果才是 &&(右值引用)。
3、推導(dǎo)是針對(duì) T 進(jìn)行,引用折疊是針對(duì)參數(shù)進(jìn)行,先進(jìn)行推導(dǎo),然后拿推導(dǎo)出的 T 對(duì)參數(shù)進(jìn)行引用折疊,得到最后的值
看個(gè)例子:
#include <iostream>
#include <string>
#include <utility> // 為了 std::forward
voidprocess_further(const std::string& s){
std::cout << "Processing as LValue (const ref): " << s << std::endl;
}
voidprocess_further(std::string&& s){
std::cout << "Processing as RValue (move): " << s << std::endl;
// 可以在這里移動(dòng)資源 s
}
template<typename T>
voidmagic_box(T&& item){
std::cout << "Inside magic_box: ";
// 僅僅打印類(lèi)型不夠直觀,我們后面會(huì)看怎么用它
// 關(guān)鍵點(diǎn):無(wú)論傳入的是左值還是右值,item 在 magic_box 函數(shù)內(nèi)部,
// 因?yàn)樗忻至?,所以它本身是一個(gè)左值!
// just_print(item); // 如果直接傳遞 item,總是傳遞左值
// 正確的做法是“完美轉(zhuǎn)發(fā)”!
process_further(std::forward<T>(item));
}
intmain(){
std::string lv_string = "I am an LValue";
magic_box(lv_string); // 傳入左值
magic_box("I am an RValue"); // 傳入右值 (字符串字面量轉(zhuǎn)臨時(shí) string)
magic_box(std::string("Another RValue")); // 傳入右值 (臨時(shí) string 對(duì)象)
std::string another_lv = "One more LValue";
magic_box(std::move(another_lv)); // 傳入被 std::move 轉(zhuǎn)換的右值
return0;
}
生活案例:萬(wàn)能引用就像是“萬(wàn)能快遞代收點(diǎn)”
想象一下,你家小區(qū)新開(kāi)了一個(gè)快遞代收點(diǎn),招牌是:“快遞代收,聯(lián)系人:李四 <模板 T> &&”。
規(guī)則:
這個(gè)李四(T&&)非常靈活,不管是是否保價(jià)(保價(jià):左值,普通:右值)的快遞(T),他都能代收。
怎么做到的?
當(dāng)你送來(lái)一個(gè)保價(jià)包裹(左值 )時(shí),李四心里會(huì)記下:“哦,這是個(gè)保價(jià)物品(T 推導(dǎo)為 Package&),我得按保價(jià)物品(Package&)的方式保管?!?/p>
當(dāng)你送來(lái)一個(gè)普通包裹(右值)時(shí),他記下:“嗯,這是個(gè)普通件(T 推導(dǎo)為 Package),按普通件(Package&&)處理就行?!?(這就是類(lèi)型推導(dǎo) + 引用折疊)
核心價(jià)值(即將引出完美轉(zhuǎn)發(fā)):
李四代收了快遞后,他的工作還沒(méi)完。他最終要把快遞交給你(或者你指定的下一個(gè)人)。這時(shí),他必須 原封不動(dòng)地 告訴你這個(gè)快遞 最初 是個(gè)保價(jià)件還是普通件。他不能把所有收到的快遞都當(dāng)成普通件(就像函數(shù)內(nèi)部參數(shù) item 總是左值一樣),也不能都當(dāng)保價(jià)件。他需要一個(gè)方法來(lái)“恢復(fù)”快遞的原始屬性。
第三:完美轉(zhuǎn)發(fā) - “信使”的神圣使命
我們從上面的 magic_box 例子看到,萬(wàn)能引用 T&& item 雖然能接收左值和右值,但在函數(shù) magic_box 內(nèi)部,item 這個(gè)參數(shù)本身,因?yàn)樗辛嗣郑妥兂闪艘粋€(gè)左值!
這就帶來(lái)一個(gè)問(wèn)題:如果 magic_box 的目的是要把接收到的 item 原封不動(dòng)地(保持其原始的左值或右值屬性)傳遞給另一個(gè)函數(shù)(比如上面例子中的 process_further),直接傳遞 item 就不行了,因?yàn)?nbsp;item 已經(jīng)是左值了。
這就是 完美轉(zhuǎn)發(fā)(Perfect Forwarding) 的用武之地,而實(shí)現(xiàn)它的工具就是 std::forward。
std::forward(item) 的作用就是:根據(jù)模板參數(shù) T 被推導(dǎo)出的原始類(lèi)型(是 int& 還是 int),將左值 item 轉(zhuǎn)換回它對(duì)應(yīng)的原始值類(lèi)別
1、如果當(dāng)初傳入 magic_box 的是左值,T 推導(dǎo)為 Type&,std::forward(item) 會(huì)返回一個(gè)左值引用。
2、如果當(dāng)初傳入 magic_box 的是右值,T 推導(dǎo)為 Type,std::forward(item) 會(huì)返回一個(gè)右值引用。
所以,萬(wàn)能引用的標(biāo)準(zhǔn)用法幾乎總是和 std::forward 成對(duì)出現(xiàn),像這樣:
template<typename T>
void forwarding_function(T&& arg) {
// ... 可能有一些自己的邏輯 ...
// 把 arg 完美轉(zhuǎn)發(fā)給下一個(gè)函數(shù)
callee_function(std::forward<T>(arg));
}
生活案例:“萬(wàn)能快遞代收點(diǎn)”的終極形態(tài)
李四(T&&)的代收點(diǎn)現(xiàn)在升級(jí)了:
接收:他能接收任何類(lèi)型的快遞(萬(wàn)能引用 T&&),并根據(jù)快遞是保價(jià)(左值)還是普通(右值)在小本本上記錄下原始類(lèi)型(模板推導(dǎo) T 為 Type& 或 Type)。
內(nèi)部處理:在他代收點(diǎn)內(nèi)部,所有快遞暫時(shí)都放在一個(gè)“已接收”區(qū)域(參數(shù) item 成為左值)。
轉(zhuǎn)發(fā):當(dāng)他要把快遞交給最終收件人或下一站時(shí),他會(huì)查小本本(看 T 的類(lèi)型),然后使用一個(gè)特殊的“轉(zhuǎn)發(fā)標(biāo)簽”(std::forward),告訴對(duì)方:“這個(gè)快遞,請(qǐng)按照它原本是保價(jià)還是普通的屬性來(lái)處理!”(完美轉(zhuǎn)發(fā))。
這樣,無(wú)論快遞經(jīng)歷了多少次代收(函數(shù)調(diào)用鏈),只要每一站都使用萬(wàn)能引用和完美轉(zhuǎn)發(fā),快遞的原始“身份”(左值/右值屬性)就能一直保持下去,直到它被最終消費(fèi)(比如被移動(dòng)構(gòu)造或拷貝構(gòu)造)。
第四:總結(jié)與區(qū)分
特性 | 右值引用 | 萬(wàn)能引用/轉(zhuǎn)發(fā)引用 |
語(yǔ)法形式 |
|
|
關(guān)鍵條件 | 類(lèi)型是確定的,沒(méi)有類(lèi)型推導(dǎo)參與 | 必須發(fā)生在模板類(lèi)型推導(dǎo)或 |
綁定對(duì)象 | 只能綁定到右值 | 既能綁定到左值,也能綁定到右值 |
推導(dǎo)行為 | 無(wú)類(lèi)型推導(dǎo) | 傳入左值時(shí), |
引用折疊 | 不涉及(因?yàn)轭?lèi)型固定) | 核心機(jī)制! |
主要目的 | 實(shí)現(xiàn)移動(dòng)語(yǔ)義,優(yōu)化資源轉(zhuǎn)移 | 實(shí)現(xiàn)完美轉(zhuǎn)發(fā),保持值類(lèi)別在函數(shù)調(diào)用鏈中傳遞 |
常用搭檔 |
|
|
生活類(lèi)比 | 閑置物品接收點(diǎn)(只收不要的) | 萬(wàn)能快遞代收點(diǎn)(啥都收,且能保持原始狀態(tài)轉(zhuǎn)發(fā)) |
如何一眼區(qū)分?
記住這個(gè)口訣:
模板推導(dǎo) 或 auto, && 變身萬(wàn)能佬;類(lèi)型寫(xiě)死 不推導(dǎo), &&就是右值寶。
當(dāng)看到 T&& 或 auto&& 時(shí),問(wèn)自己:“這里的 T 或 auto 是不是正在被編譯器推導(dǎo)出來(lái)?”
如果是,它就是萬(wàn)能引用。
如果不是,比如 void func(std::string&& s);那它就是“專(zhuān)一”的右值引用。
記?。?/h3>
右值引用&& 是 C++11 的性能優(yōu)化利器,專(zhuān)門(mén)處理“一次性用品”,配合 std::move 實(shí)現(xiàn)高效的資源轉(zhuǎn)移。
萬(wàn)能引用&& 是泛型編程和完美轉(zhuǎn)發(fā)的核心,它像個(gè)變色龍,能適應(yīng)并保持參數(shù)的原始“價(jià)值”,配合 std::forward 確保信息無(wú)損傳遞。