web-dev-qa-db-ja.com

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

このかなり役に立たないプログラムを考えてください。

#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はプログラムをコンパイルし、それはセグメンテーションフォルト
  • 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);

Update 2 :これを実現するために、自分自身を返すファンクタの書き方、またはY Combinatorの使い方を理解しています。これは言語弁護士の質問です。

Update 3 :問題は そうではない 一般的にラムダが自分自身を返すことが合法であるかどうか、しかしこれを行うこの特定の方法の合法性についてです。

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

121
n.m.

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

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

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

それがなくても、 ダングリングリファレンス があります。


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

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)がテンプレート自体を見つけるかどうかに関係なくハードエラーになる方法inがインスタンス化されるかどうか)。

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;
});
69
Barry

Edit この構造がC++仕様に従って厳密に有効であるかどうかについては、いくらか論争があるようです。一般的な意見はそれが有効ではないということのようです。より徹底的な議論のために他の答えを見てください。この答えの残りはifに当てはまります。以下の調整されたコードはMSVC++とgccで動作します、そしてOPはclangでも動作するさらに修正されたコードをポストしました。

これは未定義の動作です。内側のラムダはパラメータselfを参照によって取得しますが、selfは7行目の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
35
TypeIA

TL; DR;

クランは正しいです。

これは、この形式を不適切な形式にしている標準のセクションが [dcl.spec.auto] p9 のように見えます。

演繹されていないプレースホルダタイプを持つエンティティの名前が式に含まれている場合、プログラムは不正な形式です。 関数内で破棄されていないreturn文が見つかった場合は、その文から推定された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を追加するための提案 を見ると、それは実用的な解決策を提供します。

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と同等です(ラムダのクロージャ型が不完全であってもこれを機能させるための厄介な特別な規則があります)。

Barryは私にフォローアップの提案 Recursive lambdas を指示しました。これはなぜこれが不可能であるかを説明し、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);
};

しかし、意味論的循環性のためこれは不可能です :自動変数の型はラムダ式が処理されるまで推定されません。つまり、ラムダ式は変数を参照できません。

もう1つの自然な方法は、std :: functionを使うことです。

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

このアプローチはコンパイルしますが、通常抽象化ペナルティを導入します:std ::関数はメモリ割り当てを招くかもしれず、ラムダの呼び出しは通常間接呼び出しを必要とします。

オーバーヘッドがゼロの解決策では、ローカルクラス型を明示的に定義するよりも良い方法はありません。

21
Shafik Yaghmour

クランが正しいようです。簡単な例を考えます。

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

それをコンパイラのように見てみましょう(少し)。

  • itの型はテンプレート呼び出し演算子を持つLambda1です。
  • it(it);は呼び出し演算子のインスタンス化を引き起こします
  • テンプレート呼び出し演算子の戻り型はautoなので、推測する必要があります。
  • Lambda1型の最初のパラメータをキャプチャしたラムダを返します。
  • そのラムダは呼び出し演算子も持っていて、それは呼び出しの型を返しますself(self)
  • 注意:self(self)はまさに私たちが始めたものです。

そのため、型は推測できません。

13
Rakete1111

まあ、あなたのコードはうまくいきません。しかし、これはしません:

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);

outerはselfを値で取り、次にinnerは参照によってselfを取り込み、outerの実行が終了した後でそれを返すために進みます。そのため、分離処理は間違いなく問題ありません。

修正:

[&](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を使うだけです。

コンパイラーがラムダ式用に生成する、または生成すべきクラスに関してコードを書き直すのは十分に簡単です。

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

書き換えは循環依存がないことを示します。

#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);
}

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