這個(gè) C++ 特性如何用十年時(shí)間吃掉所有回調(diào)場(chǎng)景?
嘿,你知道嗎?在 C++11 正式請(qǐng)來(lái) lambda 這位"大咖"之前,C++ 程序員們?yōu)榱藢?shí)現(xiàn)類(lèi)似的功能,可是費(fèi)了不少勁呢!
"史前時(shí)代":函數(shù)對(duì)象(Functors)
想象一下,在沒(méi)有 lambda 的遠(yuǎn)古時(shí)期(C++11 之前),如果你想把一小段"行為"像數(shù)據(jù)一樣傳來(lái)傳去,特別是在用標(biāo)準(zhǔn)庫(kù)算法(比如 sort 或 find_if)的時(shí)候,怎么辦呢?
那時(shí)的主流方法是使用 函數(shù)對(duì)象(Function Objects),也叫 仿函數(shù)(Functors)。這其實(shí)就是重載了圓括號(hào) operator() 的類(lèi)或結(jié)構(gòu)體。它們的對(duì)象可以像函數(shù)一樣被調(diào)用。
// 一個(gè)"老派"的函數(shù)對(duì)象,用于檢查整數(shù)是否大于某個(gè)值 ??
struct IsGreaterThan {
int threshold; // 仿函數(shù)可以有自己的狀態(tài)(成員變量)
IsGreaterThan(int t) : threshold(t) {}
bool operator()(int value) const { // 重載了(),讓對(duì)象可以被調(diào)用
return value > threshold;
}
};
// 使用:
#include <vector> // 需要包含 vector 頭文件
#include <algorithm> // 需要包含 algorithm 頭文件
#include <iostream> // 用于輸出
int main() { // 將示例放入 main 函數(shù)中使其可運(yùn)行
std::vector<int> numbers = {10, 5, 25, 15};
int limit = 12;
auto first_big_number_it = std::find_if(numbers.begin(), numbers.end(), IsGreaterThan(limit)); // 創(chuàng)建一個(gè)仿函數(shù)對(duì)象
if (first_big_number_it != numbers.end()) {
std::cout << "Found number greater than " << limit << ": " << *first_big_number_it << std::endl; // 輸出 25
}
return0;
}
你看,為了實(shí)現(xiàn)一個(gè)簡(jiǎn)單的比較邏輯,就得寫(xiě)一個(gè)完整的 struct。雖然也能用,但總感覺(jué)有點(diǎn)"笨重",代碼不夠簡(jiǎn)潔,尤其是當(dāng)這個(gè)邏輯只需要用一次的時(shí)候。
C++11的"大爆炸":Lambda 登場(chǎng)!
終于,C++11 標(biāo)準(zhǔn)橫空出世,帶來(lái)了 lambda 表達(dá)式!這簡(jiǎn)直是革命性的變化!lambda 的設(shè)計(jì)目標(biāo)就是為了能 就地、簡(jiǎn)潔地定義匿名函數(shù)對(duì)象。
// 使用 C++11 lambda 實(shí)現(xiàn)同樣的功能 ??
#include <vector>
#include <algorithm>
#include <iostream>
int main() { // 同樣放入 main 函數(shù)
std::vector<int> numbers = {10, 5, 25, 15};
int limit = 12;
auto first_big_number_it = std::find_if(numbers.begin(), numbers.end(),
[limit](int value) { // 看!lambda 多簡(jiǎn)潔!
return value > limit; // 直接使用捕獲的 limit
}
);
if (first_big_number_it != numbers.end()) {
std::cout << "Found number greater than " << limit << ": " << *first_big_number_it << std::endl; // 輸出 25
}
return0;
}
對(duì)比一下,是不是清爽多了?lambda 不僅語(yǔ)法緊湊,還能方便地"捕獲"外部變量(就像上面例子里的 limit),讓編寫(xiě)簡(jiǎn)短的回調(diào)函數(shù)、自定義排序規(guī)則等變得超級(jí)方便。C++11 的 lambda 奠定了基礎(chǔ),包括捕獲列表 []、參數(shù)列表 () 和函數(shù)體 {} 這些核心要素。
C++14 及后續(xù):不斷進(jìn)化
C++ 標(biāo)準(zhǔn)委員會(huì)顯然也覺(jué)得 lambda 是個(gè)好東西,于是在后續(xù)版本中不斷給它"加技能點(diǎn)":
C++14:
- 泛型 Lambda (Generic Lambdas):參數(shù)可以用 auto 了,寫(xiě)一次就能處理多種類(lèi)型,就像我們后面會(huì)看到的 auto versatile_add 那樣。
- 捕獲初始化 (Capture Initializers):允許在捕獲列表 [] 中聲明并初始化新的變量,這些變量只在 lambda 內(nèi)部可見(jiàn)。這對(duì)于移動(dòng)捕獲(move capture)或者創(chuàng)建 lambda 內(nèi)部獨(dú)有的狀態(tài)非常有用。例如 [value = std::move(some_resource)](){ ... }。
C++17:
- constexpr Lambda:如果 lambda 滿(mǎn)足 constexpr 函數(shù)的要求,那么它可以在編譯時(shí)執(zhí)行!這對(duì)于元編程和提升運(yùn)行時(shí)性能很有幫助。
- **捕獲 *this**:按值捕獲當(dāng)前對(duì)象的副本,而不是像 [this] 那樣捕獲指針。
C++20:
- 模板 Lambda (Template Lambdas):可以直接在 lambda 上使用模板參數(shù)了,提供更強(qiáng)的泛型能力。
- 允許在 無(wú)狀態(tài) lambda(不捕獲任何東西的 lambda)和函數(shù)指針之間進(jìn)行隱式轉(zhuǎn)換。
- 允許在 lambda 捕獲列表中使用包展開(kāi) (Pack Expansion)。
所以你看,lambda 從最初為了解決函數(shù)對(duì)象寫(xiě)法繁瑣的問(wèn)題,一路進(jìn)化,功能越來(lái)越強(qiáng)大,寫(xiě)法也越來(lái)越靈活,已經(jīng)成為現(xiàn)代 C++ 編程不可或缺的一部分了!
好了,歷史課上完了,咱們接著看怎么用好這位越來(lái)越厲害的 lambda 朋友吧!
使用 lambda 的注意事項(xiàng) (歡樂(lè)版 )
好嘞,各位觀眾!咱們前面認(rèn)識(shí)了 lambda 這位 C++ 世界的新朋友,它像個(gè)可以隨身攜帶的迷你函數(shù)。不過(guò)啊,要想跟這位朋友處好關(guān)系,還得了解它的一些小習(xí)慣和"規(guī)矩"。別擔(dān)心,不復(fù)雜,跟著我來(lái)瞅瞅!
(1) lambda 的"身份證":[](){}
首先,lambda 長(zhǎng)啥樣?它有個(gè)標(biāo)志性的"身份證"——就是這對(duì)兒方括號(hào) []??吹剿珻++ 就知道:"哦豁!一個(gè) lambda 表達(dá)式要來(lái)了!"。
#include <iostream>
#include <vector>
#include <algorithm> // 需要包含 <algorithm> 頭文件
// ... 其他代碼 ...
緊跟在 [] 后面的是我們熟悉的圓括號(hào) (),用來(lái)放參數(shù),就像普通函數(shù)一樣。然后是花括號(hào) {},里面裝著 lambda 要干的活兒。
所以,最簡(jiǎn)單、最"佛系"的 lambda 長(zhǎng)這樣,它啥也不干,就圖個(gè)清靜:
auto zen_lambda = [](){}; // 一個(gè)四大皆空的 lambda... ??
你看這 [](){} 三個(gè)括號(hào)排排坐,是不是有種莫名的萌感?
當(dāng)然啦,實(shí)際中我們不會(huì)寫(xiě)這么"禪意"的 lambda。它通常會(huì)有些代碼。如果 lambda 里面還嵌套了 lambda(套娃警告?。?,記得 保持良好的縮進(jìn),不然自己都可能看暈了。必要時(shí)加點(diǎn)注釋?zhuān)瑯?biāo)明一下 lambda 的結(jié)束位置,就像給它貼個(gè)小標(biāo)簽???。
auto outer_lambda = []() // 外層 lambda 開(kāi)始啦
{
std::cout << "外面陽(yáng)光明媚~ ??" << std::endl;
auto inner_lambda = [](int x) // 里面還藏著一個(gè)!
{
std::cout << "悄悄告訴你,里面的數(shù)字翻倍是:" << x * 2 << std::endl;
return x * 2;
}; // inner_lambda 在這里結(jié)束啦
inner_lambda(5); // 調(diào)用一下里面的小家伙
}; // outer_lambda 在這里結(jié)束啦,別看丟了哦
outer_lambda(); // 跑起來(lái)看看!
(2) lambda 的"魔法背包":捕獲 []
lambda 最神奇的地方之一,就是它能"捕獲"(Capture)外面的變量,在自己的 {} 地盤(pán)里使用。這個(gè)捕獲的動(dòng)作,就發(fā)生在 [] 這個(gè)"魔法背包"里。
怎么個(gè)帶法呢?主要有兩種打包方式:
- 按值打包 [=]:這就像是把外面的東西(變量)復(fù)印一份 ??,塞進(jìn)背包。lambda 里面用的是復(fù)印件,安全!就算你對(duì)著復(fù)印件涂涂改改,外面的原件也毫發(fā)無(wú)損。缺點(diǎn)是,你改不了原件。
int pizza_slices = 8; // 外面有8片披薩 ??
auto eat_pizza_copy = [=]() { // 按值捕獲,拿到的是披薩照片
// pizza_slices -= 1; // 錯(cuò)誤!??♀? 你不能對(duì)著照片吃披薩
std::cout << "看著照片,好像有 " << pizza_slices << " 片披薩呢。" << std::endl;
};
eat_pizza_copy(); // 輸出:看著照片,好像有 8 片披薩呢。
std::cout << "外面實(shí)際上還剩 " << pizza_slices << " 片披薩。" << std::endl; // 輸出:外面實(shí)際上還剩 8 片披薩。
看到了吧?lambda 里面的 `pizza_slices` 是個(gè)拷貝,外面該多少還是多少。
- 按引用打包 [&]:這個(gè)就厲害了,相當(dāng)于給 lambda 一個(gè)直通外面的"對(duì)講機(jī)"。lambda 通過(guò)對(duì)講機(jī)直接跟外面的原件對(duì)話(huà),不僅能看到原件,還能指揮原件改變!效率高,不用復(fù)印。
int cookie_jar = 10; // 曲奇罐里有10塊小餅干 ??
auto eat_cookie_ref = [&]() { // 按引用捕獲,拿到的是罐子的鑰匙??
cookie_jar -= 1; // 直接打開(kāi)罐子,吃掉一塊!??
std::cout << "啊嗚一口,罐子里還剩 " << cookie_jar << " 塊小餅干。" << std::endl;
};
eat_cookie_ref(); // 輸出:啊嗚一口,罐子里還剩 9 塊小餅干。
std::cout << "檢查一下罐子,真的只剩 " << cookie_jar << " 塊了!" << std::endl; // 輸出:檢查一下罐子,真的只剩 9 塊了!
用 `[&]`,lambda 就能修改外面的 `cookie_jar` 了!
但是!注意!前方有坑!
按引用 [&] 捕獲雖然方便,但也藏著風(fēng)險(xiǎn)。就像你把家門(mén)鑰匙給了別人,萬(wàn)一你搬家了(變量銷(xiāo)毀了),那個(gè)人再拿著舊鑰匙回來(lái)開(kāi)門(mén),那可就"查無(wú)此房"了(程序可能就崩了)。
所以,經(jīng)驗(yàn)之談:如果 lambda 只是 "就地"使用(定義了馬上就用,用完就扔),那用 [&] 圖個(gè)方便沒(méi)問(wèn)題。但如果這個(gè) lambda 可能要"活"很久,或者被傳來(lái)傳去,那最好還是用 按值捕獲 [=] 更穩(wěn)妥?;蛘撸_一點(diǎn),在 [] 里 明確寫(xiě)出你要捕獲的變量名,是按值還是按引用,只帶必需品,別一股腦全塞包里!
int apples = 5; // 5個(gè)蘋(píng)果??
double price = 2.5; // 單價(jià)
// 只按值捕獲蘋(píng)果數(shù)量,按引用捕獲價(jià)格(可能之后要打折?)
auto calculate_cost = [apples, &price](int discount_percent) {
price = price * (1.0 - discount_percent / 100.0); // 修改引用的價(jià)格
// apples = 10; // 錯(cuò)誤! 蘋(píng)果是按值捕獲的,不能改
return apples * price;
};
double total_cost = calculate_cost(10); // 打個(gè)九折
std::cout << "打了折之后," << apples << " 個(gè)蘋(píng)果需要 " << total_cost << " 元。" << std::endl;
std::cout << "現(xiàn)在的蘋(píng)果單價(jià)是 " << price << " 元。" << std::endl;
(3) auto:lambda 的"萬(wàn)能鑰匙"
你可能注意到,我老是用 auto 來(lái)定義 lambda 變量。為啥?因?yàn)槊總€(gè) lambda 表達(dá)式,哪怕長(zhǎng)得一模一樣,在 C++ 眼里都可能有自己 獨(dú)一無(wú)二、天知地知編譯器知的類(lèi)型。我們?nèi)祟?lèi)是寫(xiě)不出這個(gè)類(lèi)型的名字的(太復(fù)雜了?。?。所以,auto 就成了我們的好幫手,它跟編譯器說(shuō):"嘿,類(lèi)型的事你看著辦,我懶得寫(xiě)了!"。編譯器心領(lǐng)神會(huì),自動(dòng)推導(dǎo)出正確的類(lèi)型。
當(dāng)然,C++ 更鼓勵(lì)我們 "匿名" 使用 lambda,用完即走,不留姓名。這樣代碼更簡(jiǎn)潔,也減少了變量名的煩惱。比如在標(biāo)準(zhǔn)庫(kù)算法里:
std::vector<int> scores = {59, 88, 76, 92, 45};
// 找到第一個(gè)及格的分?jǐn)?shù) (>= 60)
auto first_pass = std::find_if(scores.begin(), scores.end(),
[](int score) { // 看!沒(méi)有名字的 lambda,直接上!
return score >= 60;
} // 這個(gè) lambda 在這里執(zhí)行完任務(wù)就消失了,像個(gè)小精靈??
);
if (first_pass != scores.end()) {
std::cout << "找到第一個(gè)及格分?jǐn)?shù):" << *first_pass << std::endl; // 輸出 88
}
(4) 泛型 lambda:一招鮮吃遍天
C++14 更進(jìn)一步,讓 lambda 也能玩"泛型"了!咋玩?還是靠 auto 大神!在參數(shù)列表里用 auto,你的 lambda 就能處理多種類(lèi)型的數(shù)據(jù),跟個(gè)變形金剛似的!
// 這個(gè) lambda 可以給任何支持 + 號(hào)的東西做加法
auto versatile_add = [](const auto& a, const auto& b) { // 參數(shù)用了 auto!
return a + b;
};
std::cout << "整數(shù)加法: " << versatile_add(10, 20) << std::endl; // 輸出 30
std::cout << "小數(shù)加法: " << versatile_add(3.14, 1.618) << std::endl; // 輸出 4.758
std::string s1 = "你好,";
std::string s2 = "Lambda!";
std::cout << "字符串拼接: " << versatile_add(s1, s2) << std::endl; // 輸出 你好,Lambda!
是不是超方便?寫(xiě)一次,到處用!
(5) 在類(lèi)里面?別忘了 this 老兄!
如果你的 lambda 定義在類(lèi)的成員函數(shù)里,想訪問(wèn)類(lèi)的成員變量或調(diào)用其他成員函數(shù)怎么辦?很簡(jiǎn)單,把 this 指針也抓進(jìn)背包里!寫(xiě)個(gè) [this] 就行了。這樣 lambda 就知道自己是屬于哪個(gè)對(duì)象的了。
class Counter {
private:
int count = 0;
public:
auto get_incrementer() {
// 捕獲 this 指針,這樣 lambda 內(nèi)部就能訪問(wèn) count 了
return [this]() {
this->count++; // 可以訪問(wèn)并修改成員變量 count
std::cout << "Count is now: " << this->count << std::endl;
};
}
};
Counter my_counter;
auto increment = my_counter.get_incrementer();
increment(); // 輸出 Count is now: 1
increment(); // 輸出 Count is now: 2
好啦,關(guān)于 lambda 使用的小貼士就聊到這里!記住它的"身份證" [](){},玩轉(zhuǎn)"魔法背包" [] 的捕獲規(guī)則(= 值,& 引用,或者指明變量),善用 auto 和匿名 lambda,偶爾還可以試試泛型 lambda 和捕獲 this。
希望這些例子和嘮叨能讓你對(duì) lambda 更親切!多用用,你就會(huì)發(fā)現(xiàn)它的妙處了!