C ++ 14에서 초기화 캡처로 C ++ Lambda 코드 생성


9

캡처가 특히 C ++ 14에 추가 된 일반화 된 초기화 캡처에서 람다로 전달 될 때 생성되는 코드 코드를 이해 / 명확하게하려고합니다.

아래에 나열된 다음 코드 샘플을 제공하십시오. 이것은 컴파일러가 생성하는 내용에 대한 현재 이해입니다.

사례 1 : 값으로 캡처 / 값으로 기본 캡처

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

다음과 같습니다.

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int x) : __x{x}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

따라서 여러 개의 사본이 있습니다. 하나는 생성자 매개 변수에 복사하고 다른 하나는 멤버에 복사하는 것입니다. 벡터와 같은 유형에는 비용이 많이 듭니다.

사례 2 : 참조로 캡처 / 참조로 기본 캡처

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

다음과 같습니다.

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name(int& x) : x_{x}{}
    void operator()() const { std::cout << x << std::endl;}
private:
    int& x_;
};

매개 변수는 참조이고 멤버는 참조이므로 사본이 없습니다. 벡터 등의 유형에 적합

사례 3 :

일반화 된 초기화 캡처

auto lambda = [x = 33]() { std::cout << x << std::endl; };

내 입장은 이것이 회원에게 복사된다는 점에서 사례 1과 유사합니다.

내 생각에 컴파일러는 다음과 유사한 코드를 생성합니다 ...

class __some_compiler_generated_name {
public:
    __some_compiler_generated_name() : __x{33}{}
    void operator()() const { std::cout << __x << std::endl;}
private:
    int __x;
};

또한 다음이있는 경우 :

auto l = [p = std::move(unique_ptr_var)]() {
 // do something with unique_ptr_var
};

생성자는 어떻게 생겼습니까? 멤버로 이동합니까?


1
@ rafix07이 경우 생성 된 통찰력 코드는 컴파일되지 않습니다 (인수에서 고유 한 ptr 멤버를 복사 초기화하려고 시도 함). cppinsights는 일반적인 요지를 얻는 데 유용하지만 여기 에서이 질문에 대답 할 수는 없습니다.
Max Langhof 2009 년

컴파일의 첫 단계로 람다를 펑터로 변환했다고 가정하는 것 같거나 동등한 코드 (예 : 동일한 동작)를 찾고 있습니까? 특정 컴파일러가 코드를 생성하는 방법 (및 생성하는 코드)은 컴파일러, 버전, 아키텍처, 플래그 등에 따라 다릅니다. 따라서 특정 플랫폼을 요구하고 있습니까? 그렇지 않은 경우 귀하의 질문에 대한 답변이 실제로 불가능합니다. 실제로 생성 된 코드 외에는 아마도 목록에있는 함수보다 더 효율적일 것입니다 (예 : 인라인 생성자, 불필요한 사본 방지 등).
Sander De Dycker

2
C ++ 표준에 대한 내용에 관심이 있다면 [expr.prim.lambda]를 참조하십시오 . 여기에 답으로 요약하기에는 너무 많습니다.
Sander De Dycker

답변:


2

이 질문은 코드에서 완전히 대답 할 수 없습니다. 다소 "동등한"코드를 작성할 수 있지만 표준은 그런 식으로 지정되지 않습니다.

그 길을 벗어나서로 들어가 보자 [expr.prim.lambda]. 가장 먼저 주목할 점은 생성자에 대해서만 언급된다는 것입니다 [expr.prim.lambda.closure]/13.

lambda-expressionlambda-capture가 있고 그렇지 않으면 기본 기본 생성자 가 있으면 lambda-expression 과 연관된 클로저 유형에 기본 생성자가 없습니다 . 기본 복사 생성자와 기본 이동 생성자가 있습니다 ([class.copy.ctor]). lambda-expressionlambda-capture가 있고 기본적으로 copy 및 move 대입 연산자 가있는 경우 삭제 된 대입 연산자가 있습니다 ([class.copy.assign]). [ 참고 : 이러한 특수 멤버 함수는 일반적으로 암시 적으로 정의되므로 삭제 된 것으로 정의 될 수 있습니다. — 끝 참고 ]

따라서 바로 생성자에서 공식적으로 객체 캡처 방법이 정의되지 않았 음을 분명히해야합니다. 꽤 가까이 갈 수는 있지만 (cppinsights.io 답변 참조) 세부 사항은 다릅니다 (사례 4에 대한 해당 답변의 코드가 어떻게 컴파일되지 않는지 참고).


사례 1을 논의하는 데 필요한 주요 표준 조항은 다음과 같습니다.

[expr.prim.lambda.capture]/10

[...]
사본으로 캡처 된 각 엔티티에 대해 이름이 지정되지 않은 비 정적 데이터 멤버가 클로저 유형으로 선언됩니다. 이 멤버의 선언 순서는 지정되어 있지 않습니다. 엔티티가 오브젝트에 대한 참조 인 경우 이러한 데이터 멤버의 유형이 참조 된 유형이고, 엔티티가 함수에 대한 참조 인 경우 참조 된 함수 유형에 대한 lvalue 참조이거나 그렇지 않은 경우 해당 캡처 된 엔티티의 유형입니다. 익명의 노조원은 사본으로 캡처 할 수 없습니다.

[expr.prim.lambda.capture]/11

copy에 의해 캡처 된 엔티티의 odr-use 인 lambda-expression 의 compound-statement 내의 모든 id-expression 은 클로저 유형의 해당 이름이없는 데이터 멤버에 대한 액세스로 변환됩니다. [...]

[expr.prim.lambda.capture]/15

람다-표현식이 평가 될 때, 복사에 의해 캡처 된 엔티티는 결과 클로저 오브젝트의 각각의 대응하는 비 정적 데이터 멤버를 직접 초기화하는데 사용되며, 초기화 캡처에 대응하는 비 정적 데이터 멤버는 다음과 같이 초기화된다. 해당 초기화 프로그램에 의해 표시됩니다 (복사 또는 직접 초기화 일 수 있음). [...]

이것을 당신의 사건 1에 적용합시다 :

사례 1 : 값으로 캡처 / 값으로 기본 캡처

int x = 6;
auto lambda = [x]() { std::cout << x << std::endl; };

이 람다의 클로저 타입은 이름이 지정되지 않은 비 정적 데이터 멤버 (이것이라고 부르 자 __x)를 가질 것이며 ( 참조 나 함수가 아니기 int때문에 x) x람다 바디 내에서의 접근은로 접근하도록 변환된다 __x. 람다 식을 평가할 때 (즉에 할당 할 때 lambda)로 직접 초기화 __x 합니다 x.

즉, 하나의 사본 만 발생 합니다. 클로저 타입의 생성자는 포함되지 않으며, 이것을 "정상"C ++로 표현할 수 없습니다 (클로저 타입 도 집계 타입이 아닙니다 ).


참조 캡처에는 [expr.prim.lambda.capture]/12다음이 포함됩니다 .

엔티티가 내재적으로 또는 명시 적으로 캡처되었지만 사본으로 캡처되지 않은 경우 참조로 캡처됩니다. 추가로 명명되지 않은 비 정적 데이터 멤버가 참조로 캡처 된 엔티티의 클로저 유형으로 선언되는지 여부는 지정되지 않았습니다. [...]

참조의 참조 캡처에 대한 또 다른 단락이 있지만 우리는 그것을 어디서나하지 않습니다.

따라서 사례 2 :

사례 2 : 참조로 캡처 / 참조로 기본 캡처

int x = 6;
auto lambda = [&x]() { std::cout << x << std::endl; };

멤버가 클로저 유형에 추가되는지 여부는 알 수 없습니다. x람다 몸에 그냥 x외부 를 직접 참조 할 수 있습니다 . 이것은 컴파일러에게 달려 있으며 C ++ 코드의 소스 변환이 아닌 중간 언어의 형태 (컴파일러와는 다른) 로이 작업을 수행합니다.


초기화 캡처는 다음에 자세히 설명되어 있습니다 [expr.prim.lambda.capture]/6.

init-capture auto init-capture ;는 선언 영역이 람다-표현식의 복합 문인 형식 의 변수를 선언하고 명시 적으로 캡처하는 것처럼 동작 합니다.

  • (6.1) 캡처가 복사에 의한 경우 (아래 참조) 캡처에 대해 선언 된 비 정적 데이터 멤버 및 변수는 비 정적 데이터 수명이있는 동일한 객체를 참조하는 두 가지 다른 방법으로 처리됩니다. 추가 복사 및 파기는 수행되지 않으며
  • (6.2) 캡처가 참조 인 경우 클로저 객체의 수명이 끝나면 변수의 수명이 끝납니다.

이를 고려하여 사례 3을 살펴 보겠습니다.

사례 3 : 일반화 된 초기화 캡처

auto lambda = [x = 33]() { std::cout << x << std::endl; };

언급했듯이 이것을 auto x = 33;복사하여 명시 적으로 캡처 하는 변수로 생각하십시오 . 이 변수는 람다 본문 내에서만 "표시"됩니다. 앞서 언급 한 바와 같이 [expr.prim.lambda.capture]/15, 클로저 타입의 대응 멤버 ( __x후손을위한)의 초기화는 람다 식의 평가시 주어진 이니셜 라이저에 의해 이루어진다.

의심의 여지를 피하기 위해 : 여기서 물건이 두 번 초기화되는 것은 아닙니다. 는 auto x = 33;에 "것과"단순 캡처의 의미를 계승하고, 상술 한 초기화 그 의미의 변형이다. 한 번의 초기화 만 발생합니다.

여기에는 사례 4도 포함됩니다.

auto l = [p = std::move(unique_ptr_var)]() {
  // do something with unique_ptr_var
};

클로저 타입 멤버는 __p = std::move(unique_ptr_var)람다식이 평가 될 때 (즉, l할당 될 때 )에 의해 초기화된다 . p람다 본문에 대한 액세스는 에 대한 액세스로 변환됩니다 __p.


TL; DR : 최소한의 복사 / 초기화 / 이동 만 수행됩니다 (원하는대로). 람다는 소스 변환 (다른 구문 설탕과 달리)으로 정확하게 지정 되지 않았다고 가정합니다 . 생성자 측면에서 물건을 표현하면 불필요한 연산이 필요 하기 때문 입니다.

나는 이것이 질문에 표현 된 두려움을 해결하기를 바랍니다 :)


9

사례 1 [x](){} : 생성 된 생성자는 const불필요한 사본을 피하기 위해 가능한 정규화 된 참조로 인수를 승인합니다 .

__some_compiler_generated_name(const int& x) : x_{x}{}

사례 2 [x&](){} : 귀하의 가정은 정확 x하고 참조로 전달되어 저장됩니다.


사례 3 [x = 33](){} : 다시 한 번 올바른 x값으로 초기화됩니다.


사례 4 [p = std::move(unique_ptr_var)] : 생성자는 다음과 같습니다.

    __some_compiler_generated_name(std::unique_ptr<SomeType>&& x) :
        x_{std::move(x)}{}

예, unique_ptr_var폐쇄로 "이동"합니다. Effective Modern C ++에서 Scott Meyer의 항목 32도 참조하십시오 ( "init 캡처를 사용하여 오브젝트를 클로저로 이동").


"- const자격"왜?
cpplearner

@cpplearner Mh, 좋은 질문입니다. 나는 그 정신적 자율성 중 하나가 발로 들어갔 기 때문에 이것을 삽입했다고 생각합니다. 적어도 const그렇지 않을 때 약간의 모호함 / 더 나은 경기로 인해 여기에서 상처를 입을 수는 없습니다 const. 어쨌든, 당신은 내가 제거해야한다고 생각 const합니까?
lubgr 2009 년

const가 그대로 남아 있어야한다고 생각합니다. 실제로 전달 된 인수가 const라면?
Aconcagua

그래서 당신은 두 가지 이동 (또는 복사) 구조가 여기에서 발생한다고 말하고 있습니까?
Max Langhof

죄송합니다. 사례 4 (이동)와 사례 1 (사본)입니다. 내 질문의 사본 부분은 귀하의 진술에 근거하여 의미가 없습니다 (그러나 나는 그 진술에 의문을 제기합니다).
Max Langhof

5

cppinsights.io를 사용하여 추측 할 필요가 없습니다 .

사례 1 :
코드

#include <memory>

int main() {
    int x = 33;
    auto lambda = [x]() { std::cout << x << std::endl; };
}

컴파일러 생성

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

사례 2 :
코드

#include <iostream>
#include <memory>

int main() {
    int x = 33;
    auto lambda = [&x]() { std::cout << x << std::endl; };
}

컴파일러 생성

#include <iostream>

int main()
{
  int x = 6;

  class __lambda_5_16
  {
    int & x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_5_16(const __lambda_5_16 &) = default;
    // inline /*constexpr */ __lambda_5_16(__lambda_5_16 &&) noexcept = default;
    public: __lambda_5_16(int & _x)
    : x{_x}
    {}

  };

  __lambda_5_16 lambda = __lambda_5_16(__lambda_5_16{x});
}

사례 3 :
코드

#include <iostream>

int main() {
    auto lambda = [x = 33]() { std::cout << x << std::endl; };
}

컴파일러 생성

#include <iostream>

int main()
{

  class __lambda_4_16
  {
    int x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x).operator<<(std::endl);
    }

    // inline /*constexpr */ __lambda_4_16(const __lambda_4_16 &) = default;
    // inline /*constexpr */ __lambda_4_16(__lambda_4_16 &&) noexcept = default;
    public: __lambda_4_16(int _x)
    : x{_x}
    {}

  };

  __lambda_4_16 lambda = __lambda_4_16(__lambda_4_16{33});
}

사례 4 (비공식) :
코드

#include <iostream>
#include <memory>

int main() {
    auto x = std::make_unique<int>(33);
    auto lambda = [x = std::move(x)]() { std::cout << *x << std::endl; };
}

컴파일러 생성

// EDITED output to minimize horizontal scrolling
#include <iostream>
#include <memory>

int main()
{
  std::unique_ptr<int, std::default_delete<int> > x = 
      std::unique_ptr<int, std::default_delete<int> >(std::make_unique<int>(33));

  class __lambda_6_16
  {
    std::unique_ptr<int, std::default_delete<int> > x;
    public: 
    inline void operator()() const
    {
      std::cout.operator<<(x.operator*()).operator<<(std::endl);
    }

    // inline __lambda_6_16(const __lambda_6_16 &) = delete;
    // inline __lambda_6_16(__lambda_6_16 &&) noexcept = default;
    public: __lambda_6_16(std::unique_ptr<int, std::default_delete<int> > _x)
    : x{_x}
    {}

  };

  __lambda_6_16 lambda = __lambda_6_16(__lambda_6_16{std::unique_ptr<int, 
                                                     std::default_delete<int> >
                                                         (std::move(x))});
}

그리고이 마지막 코드는 귀하의 질문에 대한 답변이라고 생각합니다. 생성자에서는 [기술적으로] 이동하지 않습니다.

캡처 자체는 아니지만 기능이 const있음을 알 수 있습니다 operator(). 당연히 캡처를 수정해야하는 경우 람다를로 표시하십시오 mutable.


마지막 경우에 표시되는 코드는 컴파일되지 않습니다. "제작자가 기술적으로는 움직이지 않는다"는 결론은 그 코드에 의해 뒷받침 될 수 없다.
Max Langhof

코드 의 경우 4의 가장 확실하게 내 Mac에서 컴파일 않습니다. cppinsights에서 생성 된 확장 코드 가 컴파일되지 않는다는 것에 놀랐습니다 . 이 시점에서이 사이트는 매우 신뢰할 수있었습니다. 그들과 함께 문제를 제기 할 것입니다. 편집 : 생성 된 코드가 컴파일되지 않는다는 것을 확인했습니다. 이 편집 없이는 명확하지 않았습니다.
1

1
github.com/andreasfertig/cppinsights/issues/258 관심있는 경우 문제에 링크 SFINAE 테스트 및 암시 적 캐스트 발생 여부와 같은 사이트를 계속 추천합니다.
1
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.