1. ホーム
  2. c++

[解決済み] ラムダが自分自身を返す:これは合法か?

2022-07-27 13:42:38

質問

このかなり無駄なプログラムを考えてみましょう。

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

アップデート2 : 自分自身を返すファンクタの書き方や、Yコンビネータを使えば実現できることは理解できました。これはどちらかというと言語学者的な質問です。

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

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

どのように解決するのですか?

プログラムが不正です(clangが正しい)。 [dcl.spec.auto]/9 :

式中に非推奨のプレースホルダ型を持つ実体の名前が現れると、そのプログラムは不正な形式となる。しかし、一旦関数内で非開示の return 文が見られると、その文から推測される 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) は、それ自身を見つけたテンプレートがインスタンス化されているかどうかに関係なく、ハードエラーであることを意味します)。

については f1 に対して、コンパイラ(例えばclang)はこれをeagerlyにインスタンス化しようとすることができます。外側のラムダの型は、一旦 ; の時点で #2 上記のように(内側のラムダの型です)、それより前に使おうとしているのですが、(点であると考えて #1 の時点と考える)、つまり、内側のラムダをまだパースしている間に使おうとしているのです。これは、dcl.spec.auto/9 に反しています。

しかし f2 については、依存性があるため、eagerly にインスタンス化することはできません。私たちは使用時にのみインスタンス化することができ、その時点ではすべてを知っています。


このようなことを実際に行うためには y-コンビネータ . 論文にあった実装です。

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