C++lamda式と関数テンプレートの使い方


文書ディレクトリ
  • 前言
  • 関数オブジェクト
  • 匿名関数オブジェクトlamda
  • 基本文法は以下の
  • である.
  • lamdaの利点
  • lamdaのパラメータキャプチャ
  • 汎用lamda式
  • function関数テンプレート
  • 前言
    このセクションでは、関数オブジェクト、匿名関数オブジェクト(lamda式)、および匿名関数オブジェクトと関数テンプレートを組み合わせて、いくつかの使い方についてメモします.同時に,以下のlamda式が従来の関数オブジェクトの優劣に比べて解析される.
    肝心なのはいくつかrocksdbの似たような経典のC++プロジェクトを見て、いつもいくつかの細部がgetできないで、最終的に理解する設計思想と実際の実現に違いがあります.だからこれらの高次文法をもう一度系統的にして、ソースコードの理解を深めます.
    関数オブジェクト
    関数のオブジェクトはC++98の時すでに比較的に成熟して、先に1段のコードを見ます
    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においてもにいくつかの高誘電関数が定義され、関数オブジェクトの作成をサポートする.典型的にはbind1stbind2nd(ヘッダファイルから提供される)があり、基本的な使い方は以下の通りです.
    auto add_3 = bind2nd(plus<int>(), 3); //c++11    
    binder2nd<plus<int>()>  add_2(plus<int>(), 3); // c++98   
    
    

    3を2番目のパラメータとして関数オブジェクトplusにバインドし、返される関数オブジェクトadd_3入力ごとに+3を入力する機能もあります.各配列要素+3のために、C++98で直接関数オブジェクトを構築するコード
    //            + 3
    vector <int> arr = {1,2,3,4,5};
    transform(arr.begin(), arr.end(),
              arr.begin(),
              bind2nd(plus<int>(),3));
    

    匿名関数オブジェクトlamda
    基本文法は次の通りです.
  • lamdaは、1対の中括弧で始まる(中括弧内には内容があり得る)
  • である.
  • は、関数定義と同様にパラメータリストを持つ必要があります.[]に続く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の利点
  • はすぐに値を求めます.これにより,独立したコードをカプセル化することができ,簡潔で明瞭である.
  • マルチパス初期化の問題を解決し、関数のコピーと移動を低減し、性能を向上させる.

  • 第1の利点は,以前の例でも,加算機能関数をカプセル化し,計算した数値を直ちに返すことができることである.
    auto res = [](int x) {
        return x * x;
    }(9);
    cout << res << endl; // 9   
    

    第2の利点は、まず以下の例を見ることができます.
    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 
    #include 
    #include 
    #include 
    #include 
    
    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:参照取得変数別に参照取得変数v 1,v 2を取得し、その値を変更する.
    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 
    #include 
    #include 
    #include 
    
    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() 
                        << "
    "
    ; 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
    

    このコードは、lamda式の*thisおよびcount = get_count()が14標準の特性であるため、c++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 
    #include 
    #includ 
    
    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 + yreturn x *yに変更すると、この関数が累積和の演算であっても、5の乗算を計算する動作となる.
    function関数テンプレート
    前述したように、lamda式の構文では、各lamda式はグローバルで一意のタイプであるため、autoまたはテンプレートパラメータでのみ結果を受信できます.しかし、多くの場合、この受信プロセスがより汎用的であるため、functionテンプレートが必要である.functionテンプレートのパラメータは関数のタイプであり、1つの関数オブジェクトがfucntionに配置された後、外部ではそのパラメータ、戻り値のタイプ、実行結果しか観察されません.
    ps:functionテンプレートの作成は非常にリソースを消費するのでautoが解決できない場合は関数テンプレートを使用します
    たとえば、この例では、関数オブジェクトをmapに配置する必要があるため、function関数テンプレートを使用して完了する必要があります.
    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;
                }
            }
        };
    

    最終的にはop_dict["+"]\(1,2)を呼び出すことによって、1+2の計算を完了することができ、この方法は一般的に式の解析に役立つ.