C++ コア ガイドライン:関数オブジェクトとラムダ

現代の C++ はラムダ式なしでは考えられません。だから私の間違った仮定は、それらがラムダ式の多くのルールであるということでした.違う!ルールは 10 個未満です。しかし、いつものように、私は何か新しいことを学びました.

ラムダ式 (短いラムダ) の最初の 4 つのルールは次のとおりです。

関数オブジェクトとラムダ

  • F.50:関数が機能しない場合はラムダを使用する (ローカル変数を取得するため、またはローカル関数を記述するため)
  • F.52:アルゴリズムに渡されるものを含め、ローカルで使用されるラムダでの参照によるキャプチャを優先します
  • F.53:返される、ヒープに格納される、または別のスレッドに渡されるなど、非ローカルで使用されるラムダで参照によるキャプチャを避ける
  • ES.28:特に 04 の複雑な初期化にはラムダを使用します 変数

ラムダ関数について書きたいと言いました。見出しが関数オブジェクトとラムダと呼ばれていることに驚くかもしれません。ラムダがコンパイラによって自動的に作成された単なる関数オブジェクトであることを知っていれば、これは驚くことではありません。わからない場合は、次のセクションを読んでください。この魔法を知っていると、ラムダ式をより深く理解するのに大いに役立ちます。

ラムダ式について書く予定なので、手短にします。

内部のラムダ関数

まず、関数オブジェクトはクラスのインスタンスであり、呼び出し演算子 ( operator() ) がオーバーロードされます。これは、関数オブジェクトが関数のように動作するオブジェクトであることを意味します。関数と関数オブジェクトの主な違いは次のとおりです。関数オブジェクトはオブジェクトであるため、次のように記述できます。

以下は簡単な例です。

int addFunc(int a, int b){ return a + b; }

int main(){
 
 struct AddObj{
 int operator()(int a, int b) const { return a + b; }
 };
 
 AddObj addObj;
 addObj(3, 4) == addFunc(3, 4);
}

struct AddObj と関数 addFunc のインスタンスは両方とも呼び出し可能です。構造体 AddObj をその場で定義しました。これは、ラムダ式を使用すると、C++ コンパイラが暗黙的に行うことです。

ご覧ください。

int addFunc(int a, int b){ return a + b; }

int main(){
 
 auto addObj = [](int a, int b){ return a + b; };
 
 addObj(3, 4) == addFunc(3, 4);
 
}

以上です! ラムダ式がその環境をキャプチャし、したがって状態を持っている場合、対応する構造体 AddObj はそのメンバーを初期化するためのコンストラクターを取得します。ラムダ式がその引数を参照によってキャプチャする場合、コンストラクタも同様です。同じことが値によるキャプチャにも当てはまります。

C++14 では、一般的なラムダがあります。したがって、[](auto a, auto b){ return a + b; のようなラムダ式を定義できます。 };. AddObj の呼び出し演算子にとって、これは何を意味するのでしょうか?すでに推測できると思います。呼び出し演算子はテンプレートになります。明示的に強調したい:一般的なラムダは関数テンプレートです .

このセクションが簡潔すぎないことを願っています。 4 つのルールを続けましょう。

F.50:関数が機能しない場合はラムダを使用します (ローカル変数をキャプチャするため、またはローカル関数を記述するため)

関数とラムダ関数の使用方法の違いは、2 つのポイントに集約されます。

<オール>
  • ラムダをオーバーロードすることはできません。
  • ラムダ関数はローカル変数をキャプチャできます。
  • これは 2 番目のポイントの不自然な例です。

    #include <functional>
    
    std::function<int(int)> makeLambda(int a){ // (1)
     return [a](int b){ return a + b; };
    }
    
    int main(){
     
     auto add5 = makeLambda(5); // (2)
     
     auto add10 = makeLambda(10); // (3)
     
     add5(10) == add10(5); // (4)
     
    }
    

    関数 makeLambda はラムダ式を返します。ラムダ式は int を取り、int を返します。これは、ポリモーフ関数ラッパー std::function:std::function の型です。 (1)。 makeLambda(5) を呼び出す (2) と、a をキャプチャするラムダ式が作成されます。この場合は 5 です。同じ引数が makeLambda(10) (3) にも当てはまります。したがって、add5(10) と add10(5) は 15 (4) です。

    次の 2 つのルールは、参照によるキャプチャを明示的に扱っています。どちらも非常に似ています。したがって、それらをまとめて提示します。

    プログラムを実行するたびに、式 (5) の結果が異なります。

    ES.28:特に 11 変数

    正直なところ、コードをより堅牢にするため、このルールが気に入っています。ガイドラインが次のプログラムを悪いと呼んでいるのはなぜですか?

    widget x; // should be const, but:
    for (auto i = 2; i <= N; ++i) { // this could be some
     x += some_obj.do_something_with(i); // arbitrarily long code
    } // needed to initialize x
    // from here, x should be const, but we can't say so in code in this style
    

    概念的には、ウィジェット x のみを初期化する必要があります。初期化されている場合、一定のままである必要があります。これは C++ では表現できない考え方です。ウィジェット x がマルチスレッド プログラムで使用されている場合は、それを同期する必要があります。

    ウィジェット x が定数である場合、この同期は必要ありません。これがラムダ式の良いペンダントです。

    const widget x = [&]{
     widget val; // assume that widget has a default constructor
     for (auto i = 2; i <= N; ++i) { // this could be some
     val += some_obj.do_something_with(i); // arbitrarily long code
     } // needed to initialize x
     return val;
    }();
    

    インプレース実行ラムダのおかげで、ウィジェット x を定数として定義できます。その値を変更することはできないため、高価な同期を行わずにマルチスレッド プログラムで使用できます。

    次は?

    オブジェクト指向の重要な特徴の 1 つは継承です。 C++ コア ガイドラインには、クラス階層に関する約 25 のルールがあります。次の投稿では、クラス階層におけるインターフェースと実装の概念について書きます。