[c++] Lambda 자체 복귀 : 합법적입니까?

이 상당히 쓸모없는 프로그램을 고려하십시오.

#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는 프로그램을 컴파일하고 segfaults
  • clang은 다음 메시지와 함께 프로그램을 거부합니다.

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

어떤 컴파일러가 옳습니까? 정적 제약 조건 위반, UB 또는 둘 다 있습니까?

업데이트되는 이 약간의 수정은 그 소리에 의해 허용됩니다 :

  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 ++ lambda returning self .



답변

프로그램은 [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(내부 람다 유형)에 도달하면 외부 람다의 추론 된 유형을 알고 있지만, 그보다 더 일찍 사용하려고합니다 (at point로 생각 #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;
});


답변

편집 : 이 구조가 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


답변

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 람다는 재귀를 권장하지 않습니다. 람다 함수의 본문에서 람다 개체를 참조 할 방법이 없습니다.

그리고 그것은 Richard Smith가 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했습니다.

Lambda는 로컬 코드 리팩토링에 유용한 도구입니다. 그러나 때때로 우리는 직접 재귀를 허용하거나 클로저가 연속으로 등록되도록 허용하기 위해 자체 내에서 람다를 사용하려고합니다. 이것은 현재 C ++에서 잘 수행하기가 놀랍도록 어렵습니다.

예:

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

}

자체에서 람다를 참조하려는 자연스러운 시도 중 하나는 변수에 저장하고 참조로 해당 변수를 캡처하는 것입니다.

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

그러나 이것은 의미 적 순환 성으로 인해 가능하지 않습니다 . 자동 변수의 유형은 lambda-expression이 처리 될 때까지 추론되지 않으므로 lambda-expression이 변수를 참조 할 수 없습니다.

또 다른 자연스러운 접근 방식은 std :: function을 사용하는 것입니다.

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

이 접근 방식은 컴파일되지만 일반적으로 추상화 패널티를 도입합니다. std :: function은 메모리 할당을 유발할 수 있으며 람다를 호출하려면 일반적으로 간접 호출이 필요합니다.

오버 헤드가없는 솔루션의 경우 로컬 클래스 유형을 명시 적으로 정의하는 것보다 더 좋은 방법은 없습니다.


답변

clang이 옳은 것 같습니다. 간단한 예를 고려하십시오.

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

컴파일러처럼 살펴 보겠습니다 (약간).

  • 의 유형 it입니다 Lambda1템플릿 호출 연산자와 함께.
  • it(it); 호출 연산자의 인스턴스화를 트리거합니다.
  • 템플릿 호출 연산자의 반환 유형은 auto이므로 추론해야합니다.
  • 유형의 첫 번째 매개 변수를 캡처하는 람다를 반환합니다 Lambda1.
  • 그 람다에는 호출 유형을 반환하는 호출 연산자도 있습니다. self(self)
  • 고시 : self(self)정확히 우리가 시작한 것입니다!

따라서 유형을 추론 할 수 없습니다.


답변

글쎄, 코드가 작동하지 않습니다. 그러나 이것은 :

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

외부는 self값을 취하고 내부는 self참조로 캡처 한 다음 나중에 반환 하기 때문에 이것은 UB입니다.outer 실행이 완료된 입니다. 그래서 segfaulting은 확실히 괜찮습니다.

수정 사항 :

[&](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 ++는 반환 유형을 추론 할 때 신경 쓰지 않습니다. 단순히 코드를 한 줄씩 확인합니다.

그리고 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);
}

나는 내부 기계의 이러한 템플릿이며, 공식적인 규칙이 금지하도록 설계되었다고 생각합니다. 그들이 원래 구조를 금지한다면.


답변