ラムダが自分自身を返す:これは合法ですか?


124

このかなり役に立たないプログラムを考えてみましょう:

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

基本的に、自分自身を返すラムダを作成しようとしています。

  • MSVCはプログラムをコンパイルし、実行します
  • gccはプログラムをコンパイルし、segfaultします
  • clangは次のメッセージでプログラムを拒否します。

    error: function 'operator()<(lambda at lam.cpp:6:13)>' with deduced return type cannot be used before it is defined

どのコンパイラが正しいですか?静的制約違反、UB、またはどちらもありませんか?

このわずかな変更を更新して、clangで受け入れます。

  auto it = [&](auto& self, auto b) {
          std::cout << (a + b) << std::endl;
          return [&](auto p) { return self(self,p); };
  };
  it(it,4)(6)(42)(77)(999);

更新2:これを実現するために、自分自身を返すファンクタを作成する方法、またはYコンビネータを使用する方法を理解しています。これはより言語弁護士の質問です。

更新3:問題は、ラムダが一般的にそれ自体を返すことが合法かどうかではなく、これを行うこの特定の方法の合法性についてです。

関連質問:C ++ラムダが自分自身を返します


2
現在のところ、clangのほうがまともですが、そのような構成が型チェックさえできるかどうか、おそらく無限ツリーになってしまうのではないでしょうか。
bipll 2018

2
これが言語弁護士の質問だと言って合法かどうかを尋ねるが、いくつかの回答は実際にはそのアプローチを採用していない...タグを正しくすることが重要
Shafik Yaghmour

2
@ShafikYaghmourありがとう、タグを付けました
n。「代名詞」m。

1
@ArneVogelはい、更新されたものを使用しauto& selfて、ぶら下がっている参照の問題を排除します。
n。「代名詞」m。

1
@TheGreatDuck C ++ラムダは、実際には理論的なラムダ式ではありません。C ++には組み込みの再帰型があり、元の単純型付きラムダ計算では表現できないため、a-> aやその他の不可能な構成要素に同型のものを持つことができます。
n。「代名詞」m。

回答:


68

[dcl.spec.auto] / 9によると、プログラムの形式が正しくありません(clangは正しい)。

推測されないプレースホルダータイプを持つエンティティの名前が式に含まれている場合、プログラムの形式が正しくありません。ただし、破棄されないreturnステートメントが関数で見られると、そのステートメントから推定される戻り値の型は、他のreturnステートメントを含め、関数の残りの部分で使用できます。

基本的に、内側のラムダの戻り値の型の推定はそれ自体に依存します(ここで名前が付けられているエンティティは呼び出し演算子です)。戻り値の型を明示的に指定する必要があります。この特定のケースでは、内部ラムダのタイプが必要だが名前を付けることができないため、それは不可能です。しかし、このような再帰的なラムダを強制しようとする他の場合があります、それはうまくいくことができます。

それがなくても、ぶら下がっている参照があります。


もう少し詳しく説明します。もっと賢い人(つまりTC)と話し合った後、元のコード(わずかに削減された)と提案された新しいバージョン(同様に削減された)の間には重要な違いがあります。

auto f1 = [&](auto& self) {
  return [&](auto) { return self(self); } /* #1 */ ; /* #2 */
};
f1(f1)(0);

auto f2 = [&](auto& self, auto) {
  return [&](auto p) { return self(self,p); };
};
f2(f2, 0);

そしてそれは、内部表現があることself(self)のために依存していませんf1が、self(self, p)ために依存していますf2。式が非依存である場合、それらを使用できます...([temp.res] / 8、たとえば、static_assert(false)それ自体が見つかったテンプレートがインスタンス化されているかどうかにかかわらず、どのようにハードエラーが発生するか)。

の場合f1、コンパイラー(clangなど)はこれを熱心にインスタンス化しようとします。上記の;ポイントで外側のラムダの推定されたタイプ#2(それは内側のラムダのタイプです)を知っていますが、それよりも早くそれを使用しようとしています(ポイントと考え#1てください)。実際に型が何であるかがわかる前に、まだ内側のラムダを解析しているときに使用します。それはdcl.spec.auto/9のファウルを実行します。

ただし、はf2依存しているため、熱心にインスタンス化することはできません。インスタンス化できるのは使用時点でのみであり、その時点ですべてを知っています。


このようなことを実際に行うには、y-combinatorが必要です。論文からの実装:

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

そしてあなたが望むのは:

auto it = y_combinator([&](auto self, auto b){
    std::cout << (a + b) << std::endl;
    return self;
});

戻り値の型を明示的にどのように指定しますか?わかりません。
Rakete1111 2018

@ Rakete1111どっち?オリジナルではできません。
バリー

ああ大丈夫。私はネイティブではありませんが、「戻り値の型を明示的に提供する必要がある」ということは、方法があることを意味するようです。そのため、私は尋ねました:)
Rakete1111

4
@PedroA stackoverflow.com/users/2756719/tcはC ++の貢献者です。彼はまたAIではないか、C ++にも精通している人間がシカゴでの最近のLWGミニ会議に出席するよう説得するのに十分な機知を持っています。
ケーシー

3
@Caseyまたは、多分、人間はAIが彼に言ったことを単にオウムしている...あなたは決して知らない;)
TC

34

編集この構造がC ++仕様に従って厳密に有効であるかどうかについては、いくつかの論争があるようです。有効な意見ではないようです。より完全な議論については、他の回答を参照してください。この回答の残りの部分は、構造が有効な場合に適用されます。以下の微調整されたコードはMSVC ++とgccで動作し、OPはclangでも動作するさらに変更されたコードを投稿しました。

これは未定義の動作です。内側のラムダはself参照によってパラメーターをキャプチャしますが、7行目self以降はスコープから外れますreturn。したがって、返されたラムダが後で実行されると、スコープから外れた変数への参照にアクセスします。

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self); // <-- using reference to 'self'
      };
  };
  it(it)(4)(6)(42)(77)(999); // <-- 'self' is now out of scope
}

でプログラムを実行すると、次のvalgrindようになります。

==5485== Memcheck, a memory error detector
==5485== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5485== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5485== Command: ./test
==5485== 
9
==5485== Use of uninitialised value of size 8
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485== 
==5485== Invalid read of size 4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  Address 0x4fefffdc4 is not stack'd, malloc'd or (recently) free'd
==5485== 
==5485== 
==5485== Process terminating with default action of signal 11 (SIGSEGV)
==5485==  Access not within mapped region at address 0x4FEFFFDC4
==5485==    at 0x108A20: _ZZZ4mainENKUlT_E_clIS0_EEDaS_ENKUlS_E_clIiEEDaS_ (test.cpp:8)
==5485==    by 0x108AD8: main (test.cpp:12)
==5485==  If you believe this happened as a result of a stack
==5485==  overflow in your program's main thread (unlikely but
==5485==  possible), you can try to increase the size of the
==5485==  main thread stack using the --main-stacksize= flag.
==5485==  The main thread stack size used in this run was 8388608.

代わりに、値ではなく参照によって自己を取得するように外側のラムダを変更できます。これにより、不必要なコピーの束を避け、問題を解決できます。

#include <iostream>
int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto& self) { // <-- self is now a reference
      return [&](auto b) {
        std::cout << (a + b) << std::endl;
        return self(self);
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

これは機能します:

==5492== Memcheck, a memory error detector
==5492== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==5492== Using Valgrind-3.13.0 and LibVEX; rerun with -h for copyright info
==5492== Command: ./test
==5492== 
9
11
47
82
1004

一般的なラムダについてはよく知りませんがself、リファレンスを作成できませんでしたか?
フランソワアンドリュー2018

@FrançoisAndrieuxはい、self参照を作成すると、この問題は解消されますが、Clang は別の理由で拒否します
Justin

@FrançoisAndrieux確かに私は答えにそれを追加しました、ありがとう!
TypeIA

このアプローチの問題は、コンパイラのバグの可能性を排除しないことです。おそらくそれはうまくいくはずですが、実装は壊れています。
Shafik Yaghmour

ありがとう、私はこれを何時間も見てきましたが、参照selfによってキャプチャされているのを見ていませんでした!
n。「代名詞」m。

21

TL; DR;

clangは正しいです。

これを不正なものにする標準のセクションは[dcl.spec.auto] p9のようです:

推測されないプレースホルダータイプを持つエンティティの名前が式に含まれている場合、プログラムの形式が正しくありません。ただし、破棄されないreturnステートメントが関数で見られると、そのステートメントから推定される戻り値の型は、他のreturnステートメントを含め、関数の残りの部分で使用できます。[例:

auto n = n; // error, n’s initializer refers to n
auto f();
void g() { &f; } // error, f’s return type is unknown

auto sum(int i) {
  if (i == 1)
    return i; // sum’s return type is int
  else
    return sum(i-1)+i; // OK, sum’s return type has been deduced
}

—例を終了]

オリジナルの作品

標準ライブラリにY Combinatorを追加する提案Aを見ると、実用的なソリューションが提供されています。

template<class Fun>
class y_combinator_result {
    Fun fun_;
public:
    template<class T>
    explicit y_combinator_result(T &&fun): fun_(std::forward<T>(fun)) {}

    template<class ...Args>
    decltype(auto) operator()(Args &&...args) {
        return fun_(std::ref(*this), std::forward<Args>(args)...);
    }
};

template<class Fun>
decltype(auto) y_combinator(Fun &&fun) {
    return y_combinator_result<std::decay_t<Fun>>(std::forward<Fun>(fun));
}

そしてそれはあなたの例が不可能であると明示的に言っています:

C ++ 11/14ラムダは再帰を奨励しません。ラムダ関数の本体からラムダオブジェクトを参照する方法はありません。

そしてそれは、リチャード・スミスがclangがあなたに与えているエラーを暗示する議論を参照しています

これは第一級の言語機能としてはより良いと思います。コナ前の会議では時間切れになりましたが、ラムダに名前を付けることができるように紙を書くつもりでした(自分の体に限定されます)。

auto x = []fib(int a) { return a > 1 ? fib(a - 1) + fib(a - 2) : a; };

ここで、 'fib'はラムダの* thisと同等です(ラムダのクロージャタイプが不完全であるにもかかわらず、これを機能させるためのいくつかの迷惑な特別なルールがあります)。

バリーは私にフォローアップの提案である再帰的なラムダを指摘しました。これはなぜこれが不可能であるかを説明し、dcl.spec.auto#9制限を回避し、今日それを使わずにこれを達成する方法も示しています:

ラムダは、ロ​​ーカルコードのリファクタリングに役立つツールです。ただし、直接再帰を許可するため、またはクロージャーを継続として登録できるようにするために、ラムダをそれ自体から使用したい場合があります。現在のC ++でこれをうまく達成するのは驚くほど難しい。

例:

  void read(Socket sock, OutputBuffer buff) {
  sock.readsome([&] (Data data) {
  buff.append(data);
  sock.readsome(/*current lambda*/);
}).get();

}

それ自体からラムダを参照する自然な試みの1つは、それを変数に格納し、参照によってその変数をキャプチャすることです。

 auto on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

ただし、セマンティック循環のため、これは不可能です。自動変数の型は、ラムダ式が処理されるまで推定されません。つまり、ラムダ式は変数を参照できません。

別の自然なアプローチはstd :: functionを使用することです:

 std::function on_read = [&] (Data data) {
  buff.append(data);
  sock.readsome(on_read);
};

このアプローチはコンパイルされますが、通常は抽象化のペナルティが導入されます。std:: functionはメモリ割り当てを発生させる可能性があり、ラムダの呼び出しには通常、間接呼び出しが必要です。

オーバーヘッドゼロのソリューションの場合、ローカルクラスタイプを明示的に定義するよりも優れたアプローチはありません。


@ Cheersandhth.-Alf論文を読んだ後、標準の見積もりを見つけてしまい、標準の見積もりではどちらのアプローチも機能しない理由が明らかになるため、関連性がない
Shafik Yaghmour

「式のundeducedプレースホルダタイプが表示されますと、エンティティの名前は、プログラムが病気に形成されている場合は、」」私はプログラムしかしこのの発生は表示されません。selfそのような実体は思えない。
nと。 '代名詞' m。

@nmは可能な言葉遣いの他に、例はその言葉遣いで意味をなすようであり、例は問題を明確に示していると思います。現時点でこれ以上追加することはできないと思います。
Shafik Yaghmour

13

clangは正しいようです。簡単な例を考えてみましょう:

auto it = [](auto& self) {
    return [&self]() {
      return self(self);
    };
};
it(it);

コンパイラのように(少し)見ていきましょう。

  • のタイプitLambda1、テンプレート呼び出し演算子です。
  • it(it); 呼び出し演算子のインスタンス化をトリガーします
  • テンプレート呼び出し演算子の戻り値の型はautoであるため、推測する必要があります。
  • タイプの最初のパラメータをキャプチャするラムダを返しますLambda1
  • そのラムダには呼び出しのタイプを返す呼び出し演算子もあります self(self)
  • 通知: self(self)まさに私たちが始めたものです!

そのため、タイプを推定することはできません。


の戻り値の型Lambda1::operator()は単純Lambda2です。次に、その内側のラムダ式の中で、の戻り型でself(self)あるの呼び出しLambda1::operator()もであることがわかっていますLambda2。おそらく、正式なルールはそのささいな控除を妨げていますが、ここで提示されているロジックはそうではありません。ここでのロジックはアサーションにすぎません。正式なルールが邪魔をする場合、それは正式なルールの欠陥です。
乾杯とhth。-アルフ

@ Cheersandhth.-Alf戻り値の型がLambda2であることに同意しますが、これが提案している理由であるので、推測されない呼び出し演算子を使用できないことはわかっています。ただし、これはかなり基本的なものなので、ルールを変更することはできません。
Rakete1111 2018

9

まあ、あなたのコードは機能しません。しかし、これは:

template<class F>
struct ycombinator {
  F f;
  template<class...Args>
  auto operator()(Args&&...args){
    return f(f, std::forward<Args>(args)...);
  }
};
template<class F>
ycombinator(F) -> ycombinator<F>;

テストコード:

ycombinator bob = {[x=0](auto&& self)mutable{
  std::cout << ++x << "\n";
  ycombinator ret = {self};
  return ret;
}};

bob()()(); // prints 1 2 3

コードはUBであり、形式が正しくないため、診断は必要ありません。面白いです。ただし、どちらも個別に修正できます。

まず、UB:

auto it = [&](auto self) { // outer
  return [&](auto b) { // inner
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};
it(it)(4)(5)(6);

これはUBです。外側がself値で取得selfし、次に内側が参照でキャプチャし、outer実行が終了した後に戻ります。したがって、segfaultingは間違いなくOKです。

修正:

[&](auto self) {
  return [self,&a](auto b) {
    std::cout << (a + b) << std::endl;
    return self(self);
  };
};

コードは不正な形式のままです。これを確認するには、ラムダを展開します。

struct __outer_lambda__ {
  template<class T>
  auto operator()(T self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      T self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};
__outer_lambda__ it{a};
it(it);

これはインスタンス化します__outer_lambda__::operator()<__outer_lambda__>

  template<>
  auto __outer_lambda__::operator()(__outer_lambda__ self) const {
    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };
    return __inner_lambda__{a, self};
  }
  int& a;
};

したがって、次にの戻り値の型を決定する必要があり__outer_lambda__::operator()ます。

行ごとに説明します。まず、__inner_lambda__タイプを作成します。

    struct __inner_lambda__ {
      template<class B>
      auto operator()(B b) const {
        std::cout << (a + b) << std::endl;
        return self(self);
      }
      int& a;
      __outer_lambda__ self;
    };

さて、そこを見てください。戻り値の型はself(self)、または__outer_lambda__(__outer_lambda__ const&)です。しかし、私たちはの戻り値の型を推測しようとしてい__outer_lambda__::operator()(__outer_lambda__)ます。

あなたはそれをすることはできません。

実際にはの戻り値の型はの戻り値の型に__outer_lambda__::operator()(__outer_lambda__)実際には依存して__inner_lambda__::operator()(int)いませんが、C ++は戻り値の型を推定するときに気にしません。コードを1行ずつチェックするだけです。

そして、self(self)それを推定する前に使用されます。不良プログラム。

self(self)後で非表示にすることで、これにパッチを適用できます。

template<class A, class B>
struct second_type_helper { using result=B; };

template<class A, class B>
using second_type = typename second_type_helper<A,B>::result;

int main(int argc, char* argv[]) {

  int a = 5;

  auto it = [&](auto self) {
      return [self,&a](auto b) {
        std::cout << (a + b) << std::endl;
        return self(second_type<decltype(b), decltype(self)&>(self) );
      };
  };
  it(it)(4)(6)(42)(77)(999);
}

そして今、コードは正しく、コンパイルされます。しかし、これはちょっとしたハックだと思います。ycombinatorを使用してください。


おそらく(IDK)この説明は、ラムダに関する正式な規則に適しています。しかし、テンプレートの書き換えに関しては、内側のラムダのtemplatedの戻り値の型は、operator()それがインスタンス化されるまで(ある型の引数で呼び出されることによって)概して推定できません。したがって、手動のマシンのようなテンプレートベースのコードへの書き換えはうまく機能します。
乾杯とhth。-アルフ

@cheersあなたのコードは異なります。innerはコード内のテンプレートクラスですが、私のコードやOPコードにはありません。また、テンプレートクラスのメソッドは、呼び出されるまでインスタンス化が遅延されるため、問題になります。
Yakk-Adam Nevraumont

テンプレート関数内で定義されたクラスは、その関数外のテンプレートクラスと同等です。C ++ルールではローカルユーザー定義クラスのメンバーテンプレートを許可しないため、テンプレートコードのメンバー関数がある場合、デモコードで関数の外側で定義する必要があります。この正式な制限は、コンパイラが生成するものには適用されません。
乾杯とhth。-アルフ

7

コンパイラーがラムダ式に対して生成する、または生成する必要があるクラスの観点からコードを書き換えるのは簡単です。

それが終わったら、主な問題がぶら下がっている参照だけであること、そしてコードを受け入れないコンパイラーがラムダ部門でやや挑戦されていることは明らかです。

書き換えにより、循環依存関係がないことがわかります。

#include <iostream>

struct Outer
{
    int& a;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner( a, self );    //! Original code has dangling ref here.
    }

    struct Inner
    {
        int& a;
        Outer& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Outer& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

元のコードの内側のラムダがテンプレートタイプのアイテムをキャプチャする方法を反映する完全にテンプレート化されたバージョン:

#include <iostream>

struct Outer
{
    int& a;

    template< class > class Inner;

    // Actually a templated argument, but always called with `Outer`.
    template< class Arg >
    auto operator()( Arg& self ) const
        //-> Inner
    {
        return Inner<Arg>( a, self );    //! Original code has dangling ref here.
    }

    template< class Self >
    struct Inner
    {
        int& a;
        Self& self;

        // Actually a templated argument, but always called with `int`.
        template< class Arg >
        auto operator()( Arg b ) const
            //-> Inner
        {
            std::cout << (a + b) << std::endl;
            return self( self );
        }

        Inner( int& an_a, Self& a_self ): a( an_a ), self( a_self ) {}
    };

    Outer( int& ref ): a( ref ) {}
};

int main() {

  int a = 5;

  auto&& it = Outer( a );
  it(it)(4)(6)(42)(77)(999);
}

正式な規則が禁止するように設計されているのは、内部の機構におけるこのテンプレートだと思います。元の構成を禁止する場合。


参照してください、問題はtemplate< class > class Inner;のテンプレートoperator()がインスタンス化されていることですか?まあ、間違った言葉。書かれた?... Outer::operator()<Outer>外部演算子の戻り値の型が推定される前の間に。そしてInner<Outer>::operator()Outer::operator()<Outer>それ自体への呼び出しがあります。そしてそれは許されない。現在、ほとんどのコンパイラーは、いつが渡されたときのfor の戻り型を推測するのを待機しているため、これに気づきません。賢明です。しかし、それはコードの不適切な形を欠いています。self(self)Outer::Inner<Outer>::operator()<int>int
Yakk-Adam Nevraumont 2018

まあ、関数テンプレートがインスタンス化されるまで、関数テンプレートの戻り値の型を推測するのを待つ必要があると思いInnner<T>::operator()<U>ます。結局のところ、戻り値の型はUここに依存する可能性があります。そうではありませんが、一般的には。
乾杯とhth。-アルフ

承知しました; しかし、型が不完全な戻り型の推定によって決定される式は、引き続き違法です。一部のコンパイラは遅延していて、後でまでチェックしません。その時点までに、すべてが機能します。
Yakk-Adam Nevraumont 2018
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.