Programing

람다가 돌아오고있다 : 이것이 합법적인가?

lottogame 2020. 7. 13. 08:12
반응형

람다가 돌아오고있다 : 이것이 합법적인가?


이 쓸모없는 프로그램을 생각해보십시오.

#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가 있습니까?

이 약간의 수정은 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 : 나는 이것을 달성하기 위해 스스로를 반환하는 functor를 작성하는 방법 또는 Y 결합기를 사용하는 방법을 이해합니다. 이것은 언어 변호사 질문입니다.

3 업데이트 : 질문은 하지 람다는 일반적으로 그 자체를 반환하는 것이 합법적인지,하지만이 일이 특정 방법의 적법성에 대해.

관련 질문 : C ++ 람다가 자신을 반환합니다 .


[dcl.spec.auto] / 9에 따라 프로그램이 잘못 구성되었습니다 ( 클랜 이 맞습니다) .

교육받지 않은 자리 표시 자 유형이있는 엔터티 이름이 식에 나타나면 프로그램이 잘못 구성된 것입니다. 그러나 함수에서 폐기되지 않은 리턴 명령문이 표시되면 해당 명령문에서 추론 된 리턴 유형을 다른 리턴 명령문을 포함하여 나머지 함수에서 사용할 수 있습니다.

기본적으로 내부 람다의 반환 유형의 추론은 자체에 따라 다릅니다 (여기에서 명명 된 엔터티는 호출 연산자 임). 따라서 반환 유형을 명시 적으로 제공해야합니다. 이 특별한 경우에는 내부 람다 유형이 필요하지만 이름을 지정할 수 없기 때문에 불가능합니다. 그러나 이와 같은 재귀 람다를 강요하려고 시도하는 다른 경우가 있습니다.

그것 없이도 매달려있는 참조가 있습니다.


더 똑똑한 사람과 논의 한 후에 좀 더 자세히 설명하겠습니다 (예 : 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;
});

편집 : 이 구성이 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로 만드는 표준 섹션처럼 보입니다 .

교육받지 않은 자리 표시 자 유형이있는 엔터티 이름이 식에 나타나면 프로그램이 잘못 구성된 것입니다. 그러나 함수에서 폐기되지 않은 리턴 명령문이 표시되면 해당 명령문에서 추론 된 리턴 유형을 다른 리턴 명령문을 포함하여 나머지 함수에서 사용할 수 있습니다. [ 예:

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 조합을 추가하기 위한 제안 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 는 이것이 왜 불가능한지 설명하고 제한을 해결하는 방법을 설명하고 오늘없이 이것을 달성하는 방법을 보여주는 후속 제안 재귀 람다지적 dcl.spec.auto#9했습니다.

Lambdas는 로컬 코드 리팩토링에 유용한 도구입니다. 그러나 직접 재귀를 허용하거나 클로저를 연속으로 등록 할 수 있도록 람다 자체를 사용하려는 경우가 있습니다. 이것은 현재 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);
};

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

또 다른 자연적인 접근 방식은 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. 따라서 추론해야합니다.
  • type의 첫 번째 매개 변수를 캡처하는 람다를 반환합니다 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);

outer는 selfself을 기준으로 한 다음 참조 로 내부 캡처를 한 다음 outer실행이 끝나면 다시 반환 하기 때문에 UB 입니다. 따라서 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를 사용하십시오.


컴파일러가 람다 식에 대해 생성하거나 오히려 생성해야하는 클래스의 관점에서 코드를 다시 작성하는 것은 쉽습니다.

이것이 끝나면 주된 문제는 매달려있는 참조 일 뿐이며 코드를 수락하지 않는 컴파일러는 람다 부서에서 다소 어려움을 겪고 있음이 분명합니다.

The rewrite shows that there are no circular dependencies.

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

A fully templated version to reflect the way that the inner lambda in the original code, captures an item that's of a templated type:

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

I guess that it's this templating in the internal machinery, that the formal rules are designed to forbid. If they do forbid the original construct.

참고URL : https://stackoverflow.com/questions/52192389/lambda-returning-itself-is-this-legal

반응형