C ++ 14でのInitキャプチャによるC ++ Lambdaコードの生成


9

特にC ++ 14で追加された一般化されたinitキャプチャで、キャプチャがラムダに渡されるときに生成されるコードコードを理解/明確化しようとしています。

以下にリストする以下のコードサンプルを提供してください。これは、コンパイラーが生成するものについての私の現在の理解です。

ケース1:値によるキャプチャ/デフォルトによる値によるキャプチャ

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

次と等しい:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

そのため、複数のコピーがあります。1つはコンストラクターパラメーターにコピーし、もう1つはメンバーにコピーします。これは、ベクターなどの型にはコストがかかります。

ケース2:参照によるキャプチャ/デフォルトの参照によるキャプチャ

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

次と等しい:

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

パラメーターは参照であり、メンバーは参照であるため、コピーはありません。ベクターなどのタイプに最適

ケース3:

一般化されたinitキャプチャ

auto lambda = [x = 33]() { std::cout << x << std::endl; };

私の理解は、これがメンバーにコピーされるという意味でケース1に似ています。

私の推測では、コンパイラは次のようなコードを生成します...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

また、私が以下を持っている場合:

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

コンストラクタはどのように見えますか?会員の方にも移動しますか?


1
@ rafix07その場合、生成された洞察コードはコンパイルもされません(引数から一意のptrメンバーをコピー初期化しようとします)。cppinsightsは一般的な要点を理解するのに役立ちますが、ここの質問に明らかに答えることはできません。
Max Langhof

コンパイルの最初のステップとして、ラムダからファンクターへの変換があると想定しているようですが、同等のコード(つまり、同じ動作)を探しているだけですか。特定のコンパイラーがコードを生成する方法(およびコードが生成するコード)は、コンパイラー、バージョン、アーキテクチャー、フラグなどに依存します。そのため、特定のプラットフォームを求めていますか?そうでない場合、あなたの質問は本当に答えられません。実際に生成されたコード以外は、おそらくリストしたファンクターよりも効率的です(たとえば、インラインコンストラクター、不要なコピーの回避など)。
Sander De Dycker

2
C ++標準がそれについて述べなければならないことに興味がある場合は、[expr.prim.lambda]を参照してください。ここでは答えとして要約するには多すぎます。
Sander De Dycker

回答:


2

この質問はコードで完全に答えることはできません。多少「同等の」コードを記述できるかもしれませんが、標準はそのように指定されていません。

それが邪魔にならないので、に飛び込みましょう[expr.prim.lambda]。最初に注意すべきことは、コンストラクタはでのみ言及されているということ[expr.prim.lambda.closure]/13です。

関連した閉鎖型のラムダ式があればデフォルトコンストラクタを持っていないラムダ式があり、ラムダ・キャプチャーとそうでない不履行デフォルトコンストラクタを。デフォルトのコピーコンストラクターとデフォルトの移動コンストラクター([class.copy.ctor])があります。ラムダ式ラムダキャプチャがある場合はコピー割り当て演算子が削除され、それ以外の場合はデフォルトのコピーおよび移動割り当て演算子([class.copy.assign])が含まれます。[ 注:これらの特別なメンバー関数は通常どおり暗黙的に定義されるため、削除済みとして定義される場合があります。— エンドノート ]

つまり、すぐに、コンストラクターがオブジェクトのキャプチャーを定義する方法を正式に規定していないことは明らかです。かなり近づくことができますが(cppinsights.ioの回答を参照)、詳細は異なります(ケース4のその回答のコードがコンパイルされないことに注意してください)。


これらは、ケース1について説明するために必要な主な標準条項です。

[expr.prim.lambda.capture]/10

[...]
コピーによってキャプチャされたエンティティごとに、名前のない非静的データメンバーがクロージャタイプで宣言されます。これらのメンバーの宣言順序は指定されていません。このようなデータメンバーの型は、エンティティがオブジェクトへの参照である場合は参照型、エンティティが関数への参照である場合は参照される関数型への左辺値参照、そうでない場合は対応するキャプチャされたエンティティの型です。匿名組合のメンバーはコピーによって捕獲されないものとします。

[expr.prim.lambda.capture]/11

コピーによってキャプチャされたエンティティのodr使用であるラムダの複合ステートメント内のすべてのid式は、クロージャータイプの対応する名前のないデータメンバーへのアクセスに変換されます。[...]

[expr.prim.lambda.capture]/15

lambda-expressionが評価されると、コピーによってキャプチャされたエンティティを使用して、結果のクロージャーオブジェクトの対応する各非静的データメンバーが直接初期化され、init-capturesに対応する非静的データメンバーが次のように初期化されます。対応する初期化子によって示されます(コピーまたは直接初期化の場合があります)。[...]

これをケース1に適用してみましょう。

ケース1:値によるキャプチャ/デフォルトによる値によるキャプチャ

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

このラムダのクロージャタイプには、名前のない非静的データメンバー(と呼びましょう__x)のタイプがありintx参照でも関数でもないため)、xラムダ本体内へのアクセスはへのアクセスに変換され__xます。ラムダ式を評価するとき(つまり、に代入するときlambda)は、で直接初期化 __xxます。

つまり、1つのコピーのみが行われます。クロージャー型のコンストラクターは関係しておらず、これを「通常の」C ++で表現することはできません(クロージャー型も集約型ではないことに注意してください)。


参照キャプチャには[expr.prim.lambda.capture]/12次のものが含まれます。

エンティティは、暗黙的または明示的にキャプチャされているが、コピーによってキャプチャされていない場合、参照によってキャプチャされます。参照によってキャプチャされたエンティティのクロージャー型で、追加の無名の非静的データメンバーが宣言されるかどうかは指定されていません。[...]

参照の参照キャプチャに関する別の段落がありますが、それはどこでも行いません。

したがって、ケース2の場合:

ケース2:参照によるキャプチャ/デフォルトの参照によるキャプチャ

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

メンバーがクロージャタイプに追加されているかどうかはわかりません。xラムダ本体では、直接x外部を参照するだけかもしれません。これはコンパイラの判断次第であり、C ++コードのソース変換ではなく、何らかの形の中間言語(コンパイラごとに異なる)でこれを行います。


Initキャプチャの詳細は、[expr.prim.lambda.capture]/6次のとおりです。

init-captureはauto init-capture ;、宣言領域がラムダ式の複合ステートメントである形式の変数を宣言して明示的にキャプチャするかのように動作しますが、次の点が異なります。

  • (6.1)キャプチャがコピー(下記参照)の場合、キャプチャ用に宣言された非静的データメンバーと変数は、非静的データの存続期間を持つ同じオブジェクトを参照する2つの異なる方法として扱われます。メンバー、および追加のコピーと破棄は実行されません。
  • (6.2)キャプチャが参照によるものである場合、クロージャーオブジェクトのライフタイムが終了すると、変数のライフタイムが終了します。

それでは、ケース3を見てみましょう。

ケース3:一般化されたinitキャプチャ

auto lambda = [x = 33]() { std::cout << x << std::endl; };

述べたように、これが変数によって作成されauto x = 33;、明示的にコピーによってキャプチャされていると想像してください。この変数はラムダ本体内でのみ「可視」です。[expr.prim.lambda.capture]/15前に述べたように、(__x後世のための)クロージャー型の対応するメンバーの初期化は、ラムダ式の評価時に指定された初期化子によって行われます。

誤解を避けるために:これは、ここで2回初期化されることを意味するものではありません。これauto x = 33;は、単純なキャプチャのセマンティクスを継承する「まるで」のようなものであり、説明されている初期化はそれらのセマンティクスの変更です。初期化は1回だけ行われます。

これには、ケース4も含まれます。

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

クロージャ型のメンバーは__p = std::move(unique_ptr_var)、ラムダ式が評価されるとき(つまり、lが割り当てられるとき)に初期化されます。アクセスpラムダ体では、へのアクセスに変換されます__p


TL; DR:最小限の数のコピー/初期化/移動のみが実行されます(期待/期待どおり)。私は、ラムダがされていることを前提となりません、正確に(他のシンタックスシュガーとは違って)ソース変換の観点から指定されたため、コンストラクタの観点で物事を表現することは余分の操作が必要となります。

これが質問で示された恐れを解決することを願っています:)


9

ケース1 [x](){}:生成されたコンストラクターは、おそらくconst修飾された参照によって引数を受け入れ、不要なコピーを回避します。

__some_compiler_generated_name(const int& x) : x_{x}{}

ケース2 [x&](){}:ここでの仮定は正しく、x参照によって渡され、保存されます。


ケース3 [x = 33](){}:再び正しい、x値によって初期化されます。


ケース4 [p = std::move(unique_ptr_var)]:コンストラクターは次のようになります。

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

そう、はい、unique_ptr_varクロージャに「移動」されます。また、Effective Modern C ++のScott MeyerのItem 32も参照してください(「initキャプチャを使用してオブジェクトをクロージャに移動する」)。


" const-qualified"なぜですか?
cpplearner

@cpplearnerええ、いい質問ですね。私は、これらの精神的な自動化機能の一つが^^で蹴ったので、少なくともその挿入推測const非際に何らかの曖昧/より良い試合にここに傷つけることができないconstなどとにかく、私は削除すべきだと思いますかconst
lubgr

constは残すべきだと思いますが、実際に渡される引数がconstである場合はどうでしょうか
アコンカグア

ここで、2つの移動(またはコピー)構造がここで発生すると言っていますか?
Max Langhof

申し訳ありませんが、ケース4(移動)とケース1(コピー)を意味します。私の質問のコピー部分はあなたの発言に基づいて意味をなさない(しかし私はそれらの発言に質問します)。
Max Langhof

5

cppinsights.ioを使用すると、推測する必要が少なくなります。

ケース1:
コード

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

コンパイラが生成する

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

ケース2:
コード

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

コンパイラが生成する

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

ケース3:
コード

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

コンパイラが生成する

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

ケース4(非公式):
コード

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

コンパイラが生成する

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

そして、私はこの最後のコードがあなたの質問に答えると信じています。移動は発生しますが、コンストラクターでは[技術的に]発生しません。

キャプチャ自体はそうconstではありませんが、operator()関数がそうであることがわかります。当然、キャプチャを変更する必要がある場合は、ラムダをとしてマークしますmutable


最後のケースで表示するコードはコンパイルすらできません。「移動は発生するが、[技術的には]コンストラクタでは発生しない」という結論は、そのコードではサポートできません。
Max Langhof

ケース4 のコードは、間違いなく私のMacでコンパイルできます。cppinsightsから生成された拡張コードがコンパイルされないことに驚いています。この時点で、このサイトは私にとって非常に信頼できるものでした。それらについて問題を提起します。編集:生成されたコードがコンパイルされないことを確認しました。この編集なしではそれは明確ではありませんでした。
スウィーニッシュ

1
興味のある場合の問題へのリンク:github.com/andreasfertig/cppinsights/issues/258 SFINAEの テストなどのサイトや、暗黙的なキャストが発生するかどうかについて、サイトをお勧めします。
スウィーニッシュ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.