文章目錄
- 前言
- 函數對象
- 匿名函數對象 lamda
- 基本文法如下
- lamda 的 優點
- lamda的 參數捕獲
- 泛型 lamda 表達式
- function 函數模版
前言
本節将對函數對象,匿名函數對象(lamda表達式) 以及 匿名函數對象和 函數模版結合對一些用法 做個筆記。同時,分析以下lamda表達式相比于傳統函數對象的優劣。
關鍵是看一些 rocksdb類似的經典C++項目,老是有些細節get不到,導緻最終了解的設計思想和實際的實作有差異。是以将這些高階文法 再系統過一遍,加深對源碼的了解。
函數對象
函數對象在C++98的時候已經比較成熟了,先看一段代碼
struct adder {
adder(int n) : n_(n) {}
int operator() (int x)const {
return x + n_;
}
private:
int n_;
};
這是一個函數對象的定義,通過
adder
能夠執行個體化一個函數,
auto add_3 = adder(3)
,那麼add_3就可以作為一個+3的函數來使用。
auto add_3 = adder(3); // c++11 的初始化
adder add_3(3); // c++98的初始化
cout << " 5 add_3's result is " << add_3(5) << endl;
這裡為什麼add_3能夠使用
()
括号的文法,就是因為adder類中定義了一個
operator ()
。
同時C++98中也在
<algrorithm>
定義了一些高介函數,來支援函數對象的建立。比較典型的有
bind1st
和
bind2nd
(由<functional>頭檔案提供),基本用法如下:
auto add_3 = bind2nd(plus<int>(), 3); //c++11 的文法
binder2nd<plus<int>()> add_2(plus<int>(), 3); // c++98的文法
将3作為的第二個參數綁定到函數對象plus<int>, 傳回的函數對象add_3同樣擁有為每一個輸入+3的功能。
如下代碼,C++98中直接構造函數對象來為每個數組元素+3
// 為數組中的 每個元素 + 3
vector <int> arr = {1,2,3,4,5};
transform(arr.begin(), arr.end(),
arr.begin(),
bind2nd(plus<int>(),3));
匿名函數對象 lamda
基本文法如下
- lamda 是以一對中括号 開頭(中括号内是可以有内容的)
- 和函數定義一樣,需要擁有參數清單。緊跟在[] 之後的 int x
- 和正常函數一樣,有一個函數體,裡面會有return 語句。
- lamda表達式一般不需要說明傳回值(預設是auto 類型的)
- 每一個lamda表達式都由一個全局唯一的類型,想要精确得将 lamda表達式的傳回值捕捉到,隻能通過auto
如上
add_3
函數的功能可以 通過lamda寫出如下的實作方法
auto add_3 = [](int x) {
return x + 3;
};
cout << add_3(5) << endl;
同時如果想要實作一個通用的類似于上文中的addr類,則可以有如下實作
auto adder = [](int x) {
return [x](int n) {
return x + n;
};
};
cout << adder(3)(5) << endl; // 傳回 3 + 5
以上代碼通過x 來捕獲變量x的數值,return的操作就是将 x + n的結果放到x中,然後被外層的x捕獲。
lamda 的 優點
- 立即求值。這樣能夠将獨立的代碼封裝起來,簡潔幹淨,明了。
- 解決多重路徑初始化問題,減少函數的拷貝和移動,提升性能。
第一個優點,之前的案例也能夠展現,将加法功能函數進行封裝,并能能夠立即傳回計算的數值。
auto res = [](int x) {
return x * x;
}(9);
cout << res << endl; // 9的平方
第二優點,可以先看如下案例
Obj obj; // 預設構造函數
init_mode=1;
switch (init_mode)
{
case 1:
obj = Obj(2); // 帶參數的構造函數(我們真正想要調用的) + 指派的構造函數
break;
default:
break;
}
我們想要根據輸入,構造對應的obj對象,整個過程會調用Obj類的預設構造函數,帶參數的構造函數,指派構造函數。
那麼以上代碼可以通過lamda表達式來簡化,并且不需要預設構造函數和指派構造函數的參與,僅僅完成參數構造即可傳回。
auto obj_lamda = [init_mode]() {
switch (init_mode)
{
case 1:
return Obj(2);
break;
default:
break;
}
}();
完整測試代碼如下:
#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <functional>
#include <vector>
using namespace std;
class Obj {
public:
Obj(){cout << "default construct obj " << endl;}
Obj(int x) : x_(x) {cout << "paramter construct obj" << endl;}
Obj(Obj &&obj) {
cout << "move construct obj" << endl;
this->x_ = obj.x_;
}
Obj& operator=(const Obj &obj) {
this->x_ = obj.x_;
cout << "assign construct obj " << endl;
return *this;
}
~Obj() {}
private:
int x_;
};
int main() {
int init_mode = 1;
cout << "ordinary construct : " << endl;
Obj obj;
switch (init_mode)
{
case 1:
obj = Obj(2);
break;
default:
break;
}
cout << "lamda construct : " << endl;
auto obj_lamda = [init_mode]() {
switch (init_mode)
{
case 1:
return Obj(2);
break;
default:
break;
}
}();
return 0;
}
輸出如下:
ordinary construct :
default construct obj
paramter construct obj
assign construct obj
lamda construct :
paramter construct obj
lamda的 參數捕獲
以上的lamda表達式的案例中,lamda後面的[]中傳入參數,可以在其後{}的函數體中捕獲。
接下來看一下捕獲過程的一些細節:
變量捕獲的開頭是可選的捕獲符
=
和
&
, 其中
=
是預設的捕獲符。 這個捕獲的過程是自動按值
=
或引用
&
捕獲用到的本地變量,後面可以跟
,
進行分隔。
- 本定變量名表明對其按值捕獲(不能在預設捕獲符 = 後出現;因其已自動按值捕獲所有本地變量)
-
加本地變量名,标明對其按引用捕獲(不能在預設捕獲符&後出現,因其已自動按引用捕獲)&
- this 标明按引用捕獲外圍對象(主要是針對lamda表達式定義出現在一個非靜态類成員内部的情況),注意預設=和&會自動捕獲this對象
- *this 标明按值捕獲外圍對象(同樣針對lamda 表達式定義出現在一個非靜态類成員内部的情況)
- 變量名 = 表達式 标明按值捕獲表達式的結果(可了解為 auto var = expression)
- &變量名 = 表達式 标明按引用捕獲表達式的結果(可了解為 auto &var = expression)
這裡需要注意,一般在使用按引用捕獲 某個變量的時候需要有下面的需求:
- 需要在lamda 表達式中修改這個變量,并且這個變量要能夠讓外部觀察到
- 需要看到這個變量在外部被修改的結果
- 這個變量的複制代價比較高
舉例1: 按引用捕獲變量
按引用捕獲變量 v1,v2,并修改其值。
vector <int> v1;
vector <int> v2;
// ...
auto push_data = [&](int x) {
//這裡也可以使用 [&v1,&v2]進行捕獲
v1.push_back(x);
v2.push_back(y);
};
push_data(2);
push_data(3);
舉例2: 按值捕獲變量
檢視如下代碼,使用多個線程進行各自對象的複制,進而支援獨立運算。
#include <iostream>
#include <sstream>
#include <chrono>
#include <thread>
using namespace std;
int get_count() {
static int count = 0;
return ++count;
}
class Task {
public:
Task(int data): data_(data) {}
auto lazy_lunch() {
return
[*this, count = get_count()] () // 按值捕獲外部對象
mutable { //mutable 标記捕獲的内容可以更改
ostringstream oss;
oss << "Done work " << data_
<< " (No. " << count
<< ") in thread "
<< this_thread::get_id()
<< "\n";
msg_ = oss.str(); //更改來msg_的值
caculate();
};
}
void caculate() {
this_thread::sleep_for(100ms);
cout << msg_;
}
private:
int data_;
string msg_;
};
int main() {
auto t = Task{11};
thread t1 {t.lazy_lunch()};
thread t2 {t.lazy_lunch()};
t1.join();
t2.join();
return 0;
}
輸出如下:
#捕獲了this的引用,即this的修改可以被外部觀察。
#兩個不同的線程對象修改了各自的msg_的值,是以列印的msg_的位址内容 不同
Done work 11 (No. 1) in thread 0x70000def5000
Done work 11 (No. 2) in thread 0x70000df78000
注意這段代碼需要使用c++14的标準進行編譯,因為lamda 表達式中的
*this
和
count = get_count()
都是14标準中的特性。
以上代碼使用了lamda表達式的幾個特性:
-
标記捕獲的内容可以被更改mutable
-
表示按值捕獲外圍對象(Task)[*this]
-
捕獲表達式可以生成lamda表達式時計算并存儲等号後的表達式結果。即将get_count函數執行完,并将傳回的結果指派給count儲存下來。[count = get_count()]
如果以上代碼我們 針對
this
按值捕獲
[this]
,則不會更改
msg_
的位址
#捕獲了this的值,即使不同的線程對象在内部更改了msg_的内容,也不會被外部觀察到。
# 是以列印的 msg_ 的位址相同,也即Task 初始化時msg_的位址
Done work 11 (No. 2) in thread 0x700008cf5000
Done work 11 (No. 2) in thread 0x700008cf5000
泛型 lamda 表達式
函數的傳回值可以是auto,但是參數需要一一聲明。
這個過程在lamda表達式中進一步簡化,聲明參數是可以直接使用auto(包括auto &&),整體也就是類似于自己聲明了模版。主要還是因為lamda表達式的參數中無法使用
tmplate
關鍵字。
如下案例
template <typename T, typename V>
auto sum(T t, V v) {
return t + v;
}
跟上面函數等價的lamda表達式是:
auto sum = [](auto t, auto v) {
return x + v;
};
為什麼要推出lamda表達式的泛型,還是為了組合性的提升,以上lamda表達式 案例中的
sum
就類似于标準庫中
plus
函數模版。函數對象可以 傳遞給其他接收函數對象的函數,而
+
則無法完成這個操作。
#include <iostream>
#include <array>
#includ <numeric>
using namespace std;
int main(){
std::array<int,5> a{1,2,3,4,5};
auto s = accumulate( // 這個數學函數是進行累加和的操作
a.begin(), a.end(), 1,
[](auto x, auto y) {
return x + y;
}
);
cout << s << endl;
return 0;
}
如以上案例,使用lamda表達式構造 作為 累加和的數學函數
accumulate
的一個參數,完成所有傳入數值的相加。
但是當lamda的行為發生變化,将
return x + y
改為
return x *y
,則就變成了計算5的階乘的操作,即使這個函數是進行累加和的運算。
function 函數模版
之前說過,lamda表達式的文法中每一個lamda表達式都由一個全局唯一的類型,是以隻能用auto 或 模版參數來接收結果。但很多時候需要這個接收的過程更為通用,是以需要function模版。
function 模版的參數就是函數的類型,一個函數對象放到fucntion裡之後,外界隻能觀察到其參數、傳回值類型和執行結果。
ps :function模版的建立非常消耗資源,是以如果auto解決不了的時候再使用函數模版
std::map<string ,function<int(int, int)>>
op_dict{
{"+",
[](int x, int y) {
return x + y;
}
},
{"-",
[](int x, int y) {
return x - y;
}
},
{"*",
[](int x, int y) {
return x * y;
}
},
{ "/",
[](int x ,int y) {
assert(y!=0);
return x / y;
}
}
};