天天看點

C++ lamda表達式 和 函數模版用法

文章目錄

  • ​​前言​​
  • ​​函數對象​​
  • ​​匿名函數對象 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)

這裡需要注意,一般在使用按引用捕獲 某個變量的時候需要有下面的需求:

  1. 需要在lamda 表達式中修改這個變量,并且這個變量要能夠讓外部觀察到
  2. 需要看到這個變量在外部被修改的結果
  3. 這個變量的複制代價比較高

舉例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​

    ​标記捕獲的内容可以被更改
  • ​[*this]​

    ​ 表示按值捕獲外圍對象(Task)
  • ​[count = get_count()]​

    ​ 捕獲表達式可以生成lamda表達式時計算并存儲等号後的表達式結果。即将get_count函數執行完,并将傳回的結果指派給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;
            }
        }
    };      

繼續閱讀