Effective C++/Effective Modern C++

[Effective Modern C++] 32. 람다 초기화 캡쳐(init capture)

김디트 2025. 4. 3. 11:27
728x90

항목 32. 객체를 클로저 안으로 이동하려면 초기화 캡쳐를 사용하라

 

 

 

이동 캡쳐
  • 이동 전용 객체를 클로저 안으로 넘기고 싶다면?
    • 하지만 C++11에서는 그럴 방법이 없다.
  • 복사는 비싸고 이동은 저렴한 객체를 클로저 안으로 넘기고 싶다면?
    • 여전히 C++11에서는 그럴 방법이 없다.
  • C++14에서는 초기화 캡쳐를 통해 객체를 클로저 안으로 이동시킬 수 있다.
  • C++11에서는 이동 캡쳐를 흉내내는 우회 방법들이 있다.

 

 

 

초기화 캡쳐(init capture)
  • 일반화된 람다 캡쳐(generalized lambda capture)라고 부르기도 한다.
  • 이동 캡쳐 문제를 해결하기 위해 C++14에서 추가된 기능.
  • 아래와 같은 객체들을 지정할 수 있다.
    • 클로저 클래스에 속한 자료 멤버의 이름 ( ex) [pw = pw] )
    • 클로저 클래스에 속한 자료 멤버를 초기화하는 표현식( ex) [pw = std::move(pw)] )
  • 독특하게도 좌변과 우변이 볼 수 있는 범위가 다르다.
    • 좌변 : 클로저 클래스까지만 볼 수 있다.
    • 우변 : 람다가 볼 수 있는 범위를 모두 볼 수 있다.
class Widget
{
public:
    ...
    bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;
private:
    ...
};

auto pw = std::make_unique<Widget>();

... // pw 조작

auto func = [pw = std::move(pw)] // 초기화 캡쳐를 통한 이동 캡쳐
{ return pw->isValidated() && pw->isArchived(); };
  • 만일 pw를 조작할 필요가 없다면 아래처럼 직접 전달해 버릴 수도 있을 것이다.
auto func = [pw = std::make_unique<Widget>()]
{ return pw->isValidated() && pw->isArchived(); }

 

 

 

초기화 캡쳐를 아직 지원하지 않는 경우
  • 람다 대신 함수 객체를 생성하여 해결한다.
class IsValAndArch
{
public:
    using DataType = std::unique_ptr<Widget>;
    
    explicit IsValAndArch(DataType&& ptr) : pw(std::move(ptr)) {}
    
    bool operator()() const
    {
        return pw->isValidated() && pw->isArchived();
    }
    
private:
    DataType pw;
};

auto func = IsValAndArch(std::make_unique<Widget>());
  • 람다로 이동 캡쳐를 흉내내고 싶다면?
    1. 캡쳐할 객체를 std::bind가 리턴하는 함수 객체로 이동시키고,
    2. 그 캡쳐된 객체에 대한 참조를 람다의 매개변수로 넘긴다.
std::vector<double> data;

...

// C++14 버전
auto func = [data = std::move(data)] { ... }

// bind를 사용한 C++11 버전
auto func = std::bind(
                 [](const std::vector<double>& data) { ... },
                 std::move(data)
            );

// 직접 전달한 버전도 bind로 해결 가능하다.
// C++14 버전
auto func = [pw = std::make_unique<Widget>()]
{ return pw->isValidated() && pw->isArchived(); }

// std::bind를 사용한 C++11 버전
auto func = std::bind(
                 [](const std::unique_ptr<Widget>& pw){
                     return pw->isValidated() && pw->isArchived();
                 },
                 std::make_unique<Widget>()
            );
  • std::bind
    • 매개 변수를 전달받는 호출 가능 객체에서 매개 변수가 없는 함수 객체를 만들어낸다.(바인드 객체)
    • 첫 인수는 매개 변수를 전달받는, 호출 가능한 객체이다.
    • 나머지 인수들은 그 객체에 전달할 값들이다.
  • bind를 사용한 버전의 경우 data 매개변수가 추가되었다.
    • 이 매개변수는 바인드 객체 안의 data 복사본에 대한 lValue 참조이다.
    • std::move(data)는 rValue이지만, data의 복사본 자체는 lValue이기 때문이다.
  • operator()의 const 여부가 다르다.
    • 람다의 클로저 클래스의 operator() 멤버 함수는 const이다.
      • 따라서 내부에 캡쳐된 값들은 const가 된다.
    • 바인드 객체에 이동 생성된 data 복사본은 const가 아니다.
      • 그러므로 매개변수에 const를 붙여줘야 한다.
  • 바인드 객체는 std::bind에 전달된 모든 인수의 복사본을 저장하므로,
    • 람다가 산출한 클로저(std::bind의 첫 인수)의 복사본도 저장한다.
    • 그러므로 둘의 수명은 동일하다.
// C++14 버전
auto func = [pw = std::make_unique<Widget>()]
{ return pw->isValidated() && pw->isArchived(); }

// std::bind를 사용한 C++11 버전
auto func = std::bind([](const std::unique_ptr<Widget>& pw)
                      { return pw->isValidated() && pw->isArchived(); },
                      std::make_unique<Widget>()
            );
  • std::bind보다는 람다를 선호하는 것이 옳지만(항목 34 참조)
    • C++11의 경우 유용한 경우가 존재한다.
728x90