網站首頁 編程語言 正文
引言
C++要走向現代語言,如果不支持lambda表達式,你很難認為這門語言和現代有什么關系。幸好,從C++11標準起,它就實現了對lambda表達式的支持。
那么,什么是lambda表達式呢?
lambda表達式是匿名函數,就是說不用定義函數名,函數實現可以直接嵌入在業務邏輯代碼中。諸如python、java、C#等語言,都將其作為基礎特性。
其優點是提高了代碼的可讀性,對于一些無需重用的方法特別適合。例如在容器的迭代中實現特定的查詢邏輯。
語法與示例
C++11標準中,對于lambada表達式的定義如下:
[captures] (params) specifiers exception -> ret {body}
- [captures] —— 捕獲列表,它用于捕獲當前函數作用域的零個或多個變量,變量之間用逗號分隔。
- {params} —— 可選參數列表,其語法與普通函數參數列表一致。如果不需要參數,則可以忽略此項。
- specifiers —— 可選限定符,可選值為mutable。其意義是可以在函數體內修改按值捕獲的變量。
- exception —— 可選異常說明符,可以使用noexcept來指明lambda是否會拋出異常。
- ret —— 可選返回值類型,lambda使用返回類型后置的語法來表示返回類型。如果沒有返回值 ,則可忽略此部分。如果不指定返回類型,則編譯器會根據代碼實現為函數推導一個返回類型。
- {body} —— 表達式的函數體,此部分與實現普通函數體一致。
從上面的定義可以看到,lambda表達式的語法多少有些與我們以往的認知不太一樣。所以,我們直接上代碼來體會吧。
#include <iostream> int main() { int x = 10; auto foo = [x](int y)->int { return x + y; }; std::cout << foo(7) << std::endl; }
各位不要手懶,務必打開IDE將這段代碼運行一下,看看結果。然后再嘗試修改一下參數類型,或者返回類型。
增加mutable之后,對x變量進行修改,看看會發生什么?
int main() { int x = 10; auto foo = [x](int y) mutable ->int { x++; return x + y; }; std::cout << foo(7) << std::endl; std::cout << foo(7) << std::endl; std::cout << foo(7) << std::endl; }
而一個最簡形式的lambda表達式,可以是 auto foo = [] { return 0; };
。
所以大家以后看到類似語法,可不要大驚小怪了,還以為這是什么另類的數組訪問方式,或者琢磨->這個指針指向了個什么東西。
捕獲列表
毫無疑問,lambda表達式中,最反直覺的就是捕獲列表的定義。畢竟在我們的認知里,中括號是用來定義數組并訪問數組元素的。
而且捕獲列表的諸多特性,也是面試中挖坑的好地方。我們先從作用域開始說起。
關于lambda表達式的作用域,有四個重點:
- 第一,捕獲列表的變量有兩個作用域,一是lambda定義的函數作用域,二是表達式函數體所在代碼的作用域;
- 第二,在表達式函數體內,默認情況下,被捕獲變量是只讀屬性。如需修改,則要添加mutable標識;
- 第三,在表達式函數體內,修改被捕獲變量的值,不影響原始變量的值;
- 第四,被捕獲變量必須是非靜態局部變量。
好,為了加強印象,我們先從面試挖坑場景開始。請先看第一坑:
int main() { int x = 10; auto foo = [x](int y) ->int { x++; return x + y; }; std::cout << foo(8) << std::endl; }
問:上述代碼能通過編譯嗎?如果不能,是為什么?
答:不能。因為根據規則一和二,在表達式函數體內,x變量是只讀,不能被修改。
好,上面的只是開胃菜,詭異的第二坑來了:
int main() { int x = 10; auto foo = [x](int y) mutable ->int { x++; std::cout << x << std::endl; return x + y; }; foo(8); foo(8); std::cout << x << std::endl; }
問:在上述代碼執行完后,請說出所有std::cout
語句輸出什么內容?
答:根據規則三,分別輸出11,12,10。
所以大家可以看到,前兩次輸出是在表達式體內對x值進行修改,x的狀態是保留的了。但在函數體外,x變量的值仍然保持不變。千萬要記住這個規則,這就是存在兩個作用域所得到的結果。
后面我們會講解其實現原理,這樣就可以方便記憶這條規則了。
再來第三坑:
int y = 100; int main() { static int x = 10; auto foo = [x, y](int z) ->int { return x + y + z; }; std::cout << foo(8) << std::endl; }
問:上述代碼有什么問題,應該如何調整?
答:根據作用域規則四,x與y都不能作為lambda表達式的捕獲列表變量。在表達式函數體內可以直接使用靜態或者全局變量,所以只要修改為auto foo = [](int z) ->int { return x + y + z; };
即可。
雖然上面這段代碼放在gcc編譯器下不會報錯,只是出警告:capture of variable ‘x’ with non-automatic storage duration
。但編譯器實際上是作了一次選擇,即認為你的意圖是作為全局或靜態變量來使用。可如果你只是手誤或者忘了要調整代碼,那就會出現預料之外的運行結果。
實際上,筆者所在公司就嚴格要求不能出現編譯警告,這對于防止運行期出現偏差是非常重要的。
捕獲引用
上一節我們所講的,都是捕獲值的用法。那么想將變量作為引用傳遞進lambda表達式,是否可以呢?
答案是肯定的,示例如下:
int main() { int x = 10; int y = 11; auto foo = [&x, &y](int z) mutable ->int { x++; y++; return x + y + z; }; std::cout << foo(8) << std::endl; std::cout << x << std::endl; std::cout << y << std::endl; }
可以看到,捕獲引用就是在變量前加上&符號即可。切記不要與取地址相混淆。被引用變量在函數體內修改,也會影響函數體外同名變量的值,這一點與我們以往的認知相同。
至于上述代碼結果,請各位先在腦海里執行一遍,然后上機驗證。
特殊用法
捕獲列表支持三種特殊情況,下面分別說明:
- [this] —— 捕獲this指針,可以在類內的lambda表達式函數體內使用,以訪問成員變量和方法;
- [=] —— 捕獲當前作用域內全部變量的值,包括this。當變量較多時,可使用此符號;
- [&] —— 捕獲當前作用域內所有變量的引用,包括this。
先看[this]的示例:
class CaptureOne { public: void test() { auto foo = [this] { std::cout << x << std::endl; }; foo(); } private: int x = 100; }; int main() { CaptureOne one; one.test(); }
建議大家在上面這個例子里繼續擴展,例如增加一個類方法,看看在表達式函數中如何調用,以及執行結果是什么。
再看[=]的示例:
int main() { int x = 10; int y = 11; int z = 12; auto foo = [=]() mutable ->int { x++; y++; return x + y + z; }; std::cout << foo() << std::endl; std::cout << x << std::endl; std::cout << y << std::endl; }
可以看到,這的確是方便了代碼書寫。如果表達式函數體內的變量增加或減少,都不必再費心思去調整捕獲列表了。
另外,不知道大家還注意到一個細節沒有,就是當參數列表為空時,如果存在mutable標識,則()是不能省略的。
最后,[&]還是煩請各位自己去實踐并體驗。
實現原理
C++多年老手,在上手lambda表達式的時候,應該會馬上在腦海里跟函數對象關聯起來。
我們先從定義一個函數類開始說起。所謂函數類,就是定義一個類,然后重載operator ()。這樣可以將類對象作為函數一樣來調用。
示例如下:
class FuncOne { public: FuncOne(int x, int y) : valx(x), valy(y){} int operator () () { return valx + valy; } private: int valx; int valy; }; int main() { FuncOne one(10, 20); std::cout << one() << std::endl; }
上述代碼就是函數類的定義與函數對象的使用方法。而C++11標準中的lambda表達式,其實就是在編譯器在編譯期生成一個閉包類(可以理解為類似于FuncOne的類),然后在運行時由這個閉包類生成一個對象,稱之為閉包。
而閉包就是一個匿名的函數對象,它包含了定義時作用域的上下文。這樣我們就會發現lambda表達式,其實就是C++11給我們提供的一個語法糖。
現在再回頭去理解捕獲列表的四條原則,是否有恍然大悟之感?理解了原理,對于原則就不用死記硬背了,祝各位面試好運!
應用
當然,我們不能滿足于只過面試關,還應當在工作中用好它才行。做C++開發的,用好STL是基本功,而lambda表達式與STL相結合,可以簡化代碼,發揮出強大的威力來。
先看一個需求:我們要在一組vector列表中,打印可以整除4的數。
示例代碼:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<int> num_list = {1, 3, 9, 10, 12, 17, 18}; std::cout << *std::find_if(num_list.cbegin(), num_list.cend(), [](int i) { return (i % 4) == 0;}) << std::endl; }
大家可以看到,如果是在以前,要實現這個功能,需要額外寫多少代碼。
也希望各位能從這個簡單示例開始,回頭去梳理自己以前寫的代碼,可以怎樣優化結構并提高效率。并且在將來的開發過程中,不斷修煉功夫,成為C++大咖!
原文鏈接:https://blog.csdn.net/michaeluo/article/details/124634006
相關推薦
- 2022-10-22 Kotlin淺析null操作方法_Android
- 2023-03-13 pandas行和列的獲取的實現_python
- 2022-04-11 error: failed to push some refs to如何解決
- 2021-12-15 詳談浮點精度(float、double)運算不精確的原因_C 語言
- 2022-07-15 SQL?Server數據表壓縮_MsSql
- 2022-03-26 C語言實現字符串替換的示例代碼_C 語言
- 2022-11-06 SQL?Server?Reporting?Services?匿名登錄的問題及解決方案_MsSql
- 2023-11-19 DOCKER權限問題:權限不夠Got permission denied while trying
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支