快用一用 lambda 表達式吧,讓你的代碼更簡潔、更漂亮!

目錄

lambda 表達式

定義 lambda 表達式

捕獲子句

按值捕獲

按引用捕獲

捕獲特定的變量

捕獲this指針

結合 lambda 使用 STL 算法


lambda 表達式

lambda 表達式提供了一種便捷、簡潔的語法來快速定義回調函數或函數對象。而且,不只語法簡潔,lambda 表達式還允許在使用回調的地方定義回調的邏輯。這通常比在某個類定義的函數調用運算符的某個地方定義這種邏輯要好得多。因此,使用 lambda 表達式一般能夠得到極具表達力且可讀性仍然很好的代碼。看下面這個例子:

bool compare(int a,int b)
{
    return a>b;
}
//使用函數
sort(asd.begin(), asd.end(), compare);

//使用lambda表達式
sort(asd.begin(), asd.end(), [](int &a, int &b)->bool {return a > b;});

lambda 表達式與函數定義有許多相似的地方。最基本的 lambda 表達式提供了一種定義沒有名稱的函數(即匿名函數)的方法。在應用中會發現 lambda 表達式與普通函數不同,因爲 lambda 表達式可以訪問自己定義的作用域內存儲的變量。lambda 表達式的計算結果是一個函數對象。該函數對象的正式名稱是 lambda 閉包,但是有很多人也稱之爲 lambda 函數。

 

 

定義 lambda 表達式

一個基本的 lambda 表達式:

[] (int x, int y) { return x < y; }

可以看出,lambda 表達式的定義看上去很像函數的定義。主要區別在於,lambda 表達式不指定返回類型和函數名稱,並且始終以方括號開頭。表達式開始的" [ ] "稱爲 lambda 引導,它們標記了 lambda 表達式的開頭。lambda 引導的內容並不總是爲空的。lambda 引導後跟的 " ( ) " 是 lambda 參數列表。對於沒有參數的 lambda 表達式,可以省略空的參數列表 ()。即可以將形式爲 [](){......},的 lambda 表達式進一步縮減爲 []{......}。空的lambda引導不能省略,因爲它標記lambda表達式的開始。

有的人可能會覺得 lambda 表達式沒有函數名稱還可以理解,但是沒有返回類型是不是就有點過分了(玩呢?)。lambda 表達式可以包含任意數量的語句,返回類型默認爲返回值的類型。如果沒有返回值,返回類型爲 void。不過返回值類型也是可以根據需求設定的,例如下面這種情況:

[](double x) -> int { int y = x; return x-y;}

 

 

捕獲子句

lambda 引導 [] 不一定是空的,它可以包含捕獲子句,以指定封閉作用域中的變量如何在 lambda 表達式中訪問。如果方括號爲空,則lambda 表達式體只能使用 lambda 表達式中局部定義的實參和變量。沒有捕獲子句的 lambda 表達式稱爲無狀態的 lambda 表達式,因爲它不能訪問其封閉作用域中的任何內容。默認捕獲子句有兩種:= 和 &。捕獲子句只能包含一種默認捕獲子句,不能同時包含兩者。

 

按值捕獲

如果在方括號中包含 =,lambda 表達式體就可以按值訪問封閉作用域中的所有自動變量,即這些變量的值可以在 lambda 表達式中使用,但不能修改存儲在原始變量中的值。當你按值捕獲變量時,又試圖改變它,編譯器就會報錯:

int a = 1,b = 2,c = 3;
[ =,&c ](){ a++; b++; c++; cout << a << " " << b << " " << c <<endl; } ();
cout << a << " " << b << " " << c <<endl;

= 捕獲子句允許在 lambda 表達式體中按值訪問 lambda 表達式定義所在作用域的所有變量。在上面的例子中,在原則上,lambda 表達式體能夠訪問 main() 的 3 個局部變量:a、b 和 c。按值捕獲局部變量的效果與按值傳遞實參大不相同。

不同點在於:對於 lambda 表達式體內用到的封閉作用域內的每個局部變量,閉包對象都有一個成員。稱爲 lambda 捕獲了這些變量。至少在概念上,所生成的成員變量的名稱與捕獲到的變量的名稱相同。這樣一來,lambda 表達式體看起來訪問的是封閉作用域中的變量,但實際上訪問的是 lambda 閉包內存儲的對應的成員變量。

 

按引用捕獲

如果在方括號中放置 &,封閉作用域中的所有變量就可以按引用訪問,所以它們的值可以由lambda表達式體中的代碼修改。例如:

int a = 1,b = 2,c = 3;
//按引用捕獲
[ & ](){ a++; b++; c++; } ();
cout << a << " " << b << " " << c <<endl;

外層作用域內的所有變量都可按引用訪問,所以 lambda 可以使用和修改它們的值。雖然 & 捕獲子句是合法的,但是在外層作用域按引用捕獲所有變量並不是一種好方法,因爲這將使得變量有可能被無意修改。類似地,使用 = 默認捕獲子句則可能引入高開銷的複製操作。因此,更安全的做法是顯式指定如何捕獲自己需要的每個變量。

 

捕獲特定的變量

通過在捕獲子句中逐個列舉,可以指定想要訪問的封閉作用域中的特定變量。對於每個變量,可以選擇按值還是按引用捕獲。在變量名前面加上 &,就可以按引用捕獲變量。例如:

auto counter { [ &count] (int x, int y) {++count; return x < y;} };

count 是封閉作用域中可以在 lambda 表達式體中訪問的唯一變量,&count 規範使之可以按引用訪問。沒有 &,外層作用域中的 count 變量就按值訪問。當你想要按值捕獲特定變量時,不能在變量名前面加 = 作爲前綴。例如,捕獲子句[ = numbers ]是無效的,正確的語法是[ numbers ]。

在捕獲子句中放置多個變量時,就用逗號分開它們。可以自由混合按值捕獲的變量和按引用捕獲的變量。還可以在捕獲子句中,同時包含默認捕獲子句和捕獲的特定變量名稱。例如:捕獲子句[ =, &counter ]允許按引用訪問 counter,按值訪問封閉作用域中的其他變量。類似的,捕獲子句[ &, numbers ]的意思是,按值捕獲 numbers,按引用捕獲其他變量。如果指定默認捕獲子句( = 或 &),則它必須是捕獲列表中的第一項。

注意:如果使用 = 默認捕獲子句,則不能再按值捕獲任何特定變量;類似地,如果使用 &,則不能再按引用捕獲特定變量。例如:[ =, = a ] 或 [ &, & b ]。

 

捕獲this指針

當我們在類中的成員函數中使用 lambda 表達式時,這個情況和之前的不一樣,問題在於只有局部變量和函數實參才能按值或引用捕獲,而類的成員變量不能按值或按引用捕獲。以下是在類的成員函數中使用 lambda 的情況:

可以通過對捕獲子句添加關鍵字 this ,讓 lambda 表達式訪問類的成員。通過捕獲 this 指針,實際上就使得 lambda 表達式能夠訪問包含它的成員函數所能訪問的所有成員,也就是說,儘管 lambda 閉包不屬於類,但其函數調用運算符仍然能夠訪問類的 protected 和 private 數據成員。lambda 表達式還能夠訪問所有成員函數,無論它們被聲明爲 public、protected 還是 private。

class Asd
{
	private:
		int a, b;
	public:
		Asd(){
			a = 1;
			b = 2; 
		}
		int geta(){
			return a;
		}
		int getb(){
			return b;
		}
		void print(){
			auto asd = [this](){ return getb() - geta();} ;
			cout << "getb() - geta():" << asd() <<endl;
		}
		void print1(){
			auto asd = [=](){ return b - a;} ;
			cout << "b-a:" << asd() <<endl;
		}
};

注意:= 默認捕獲子句已經暗示着(按值)捕獲this指針。因此這條語句也是合法的:

auto asd = [=](){ return b - a;} ;

但不允許將默認捕獲子句 = 與 this結合使用 (至少在C++17中不允許,C++20中可能會有變化) 。因此,編譯器將把[=, this]這種形式的捕獲子句標記爲錯誤。不過,允許使用[&, this],因爲 & 並不暗示着捕獲this。

 

捕獲子句小總結:

[]        不捕獲任何變量
[=]       用值的方式捕獲所有變量
[&]       以引用方式捕獲所有變量
[asd]     以值方式捕獲asd; 不捕獲其它變量
[=,&asd]  以引用捕獲asd, 但其餘變量都靠值捕獲
[&, asd]  以值捕獲asd, 但其餘變量都靠引用捕獲
[this]    捕獲所在類的this指針

 

完整代碼:

#include <iostream>
using namespace std;

class Asd
{
	private:
		int a, b;
	public:
		Asd(){
			a = 1;
			b = 2; 
		}
		int geta(){
			return a;
		}
		int getb(){
			return b;
		}
		void print(){
			auto asd = [this](){ return getb() - geta();} ;
			cout << "getb() - geta():" << asd() <<endl;
		}
		void print1(){
			auto asd = [=](){ return b - a;} ;
			cout << "b-a:" << asd() <<endl;
		}
};

int main()
{
	//不捕獲任何變量
	[] { cout << "Study lambda!" <<endl; } ();
	auto print = [] { cout << "Study lambda!" <<endl; };	
	print();

	//根據需求確定返回值
	auto asd = [](double x) -> int { int y = x; return x-y;};
	cout << asd(3.6) <<endl;

	int a = 1,b = 2,c = 3;
	//按值捕獲
	[ = ](){ cout << a << " " << b << " " << c <<endl; } ();

	//按引用捕獲
	[ & ](){ a++; b++; c++; } ();
	cout << a << " " << b << " " << c <<endl;

	//捕獲特定的變量
	[ =,&c ](){ c++; cout << a << " " << b << " " << c <<endl; } ();
	cout << a << " " << b << " " << c <<endl;

	[ &,a ](){ b++; c++; cout << a << " " << b << " " << c <<endl; } ();
	cout << a << " " << b << " " << c <<endl;

	//捕獲this指針
	Asd asd1, asd2;
	asd1.print();
	asd2.print1();

	return 0;
}

 

 

結合 lambda 使用 STL 算法

接下來將從 距離 和 簡潔 兩個方面探討使用 lambda 的優勢。

很多人認爲,讓定義位於使用的地方附近很有用。這樣,在閱讀源代碼時,就無需去找該函數功能的具體代碼。例如,調用 count_if()的第三個參數時,不需要向前尋找具體實現。另外,如果需要修改代碼,涉及的內容都在附近。從這一角度出發,lambda 是理想的選擇,因爲其定義和使用是在同一個地方進行的。而函數可能存在一種比較糟糕的情況,即其函數內部使用了其他函數,而這些函數可能在不同的地方,這在閱讀源碼這一點上是非常費時、費力的。

從簡潔的角度看,函數和lambda的簡潔程度相當,一個例外是,需要使用同一個 lambda 兩次:

count1 = count_if ( a.begin(), a.end(), [] (int x) { return x % 3 == 0; });
count2 = count_if ( b.begin(), b.end(), [] (int x) { return x % 3 == 0; });

但並不一定要編寫lambda兩次,而是給lambda指定一個名稱,並使用該名稱兩次:

auto mod3 = [] (int x) { return x % 3 == 0; }
count1 = count_if ( a.begin(), a.end(), mod3 );
count2 = count_if ( b.begin(), b.end(), mod3 );

甚至可以像使用常規函數那樣使用有名稱的 lambda:

bool result = mod3(z);

然而,不同於常規函數,可在函數內部定義有名稱的 lambda 。mod3 的實際類型隨實現而異,它取決於編譯器使用什麼類型來跟蹤lambda 。

代碼:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

bool compare(int a, int b)
{
    return a > b;
}

int main() 
{	
	vector<int> asd;
	for (int i = 0; i < 10; ++i)
		asd.push_back(i);
	
	sort(asd.begin(), asd.end(), [](int &a, int &b)->bool {return a > b;});
	sort(asd.begin(), asd.end(), compare);

	for (int i = 0; i < asd.size(); ++i)
		cout << asd[i] << endl;
	return 0;
}

 

代碼:


#include <iostream>
#include <numeric>
#include <vector>
 
void print_container(const std::vector<char>& c)
{
    for (auto x : c) {
        std::cout << x << ' ';
    }
    std::cout << '\n';
}
 
int main()
{
    std::vector<char> cnt(10);
    std::iota(cnt.begin(), cnt.end(), '0');
 
    std::cout << "Init:\n";
    print_container(cnt);
 
    std::erase(cnt, '3');
    std::cout << "Erase \'3\':\n";
    print_container(cnt);
 
    auto erased = std::erase_if(cnt, [](char x) { return (x - '0') % 2 == 0; });
    std::cout << "Erase all even numbers:\n";
    print_container(cnt);
    std::cout << "In all " << erased << " even numbers were erased.\n";
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章