이 상당히 쓸모없는 프로그램을 고려하십시오.
#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);
}
나는 내부 기계의 이러한 템플릿이며, 공식적인 규칙이 금지하도록 설계되었다고 생각합니다. 그들이 원래 구조를 금지한다면.