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