KoreanFoodie's Study

Effective Modern C++ | 항목 32 : 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라 본문

Tutorials/C++ : Advanced

Effective Modern C++ | 항목 32 : 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라

GoldGiver 2022. 10. 26. 10:05

C++ 프로그래머의 필독서이자 바이블인, 스콧 마이어스의 Modern Effective C++ 를 읽고 기억할 내용을 요약하고 있습니다. 꼭 읽어보시길 추천드립니다!

항목 32 : 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라

핵심 :

1. 객체를 클로저 안으로 이동할 때에는 C++14 의 초기화 갈무리를 사용하라.
2. C++11 에서는 직접 작성한 클래스나 std::bind 로 초기화 갈무리를 흉내낼 수 있다.

 

람다 사용시 값 갈무리와 참조 갈무리가 마땅치 않은 경우가 있다. 이동 전용 객체(std::unique_ptr, std::future 등)이 좋은 예이다. C++14 에서는 객체를 클로저 안으로 이동하는 수단을 직접 제공한다. C++11 에서도 이동 갈무리를 흉내낼 수 있다. 초기화 갈무리(init capture) 을 이용하면 된다. 초기화 갈무르리로는 다음과 같은 것들을 지정할 수 있다.

  1. 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름
  2. 그 자료 멤버를 초기화하는 표현식
class Widget {
public:
  bool a() const;
  bool b() const;
  bool c() const;
};

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

auto func = [pw = std::move(pw)]
  { return pw->a() && pw->b(); };

캡쳐 리스트 '[ ]' 안이 초기화 갈무리이다. 흥미로운 점은, '=' 의 좌변과 우변의 범위가 다르다는 것인데, 좌변의 범위는 해당 클로저 클래스의 범위이고, 우변의 범위는 람다가 정의되는 지점의 범위와 동일하다. 즉, "pw = std::move(pw)" 는 "클로저 안에서 자료 멤버 pw 를 생성하되, 지역 변수 pw 에 std::move 를 적용한 결과로 그 자료 멤버를 초기화하라" 라는 뜻이다.

다음과 같은 방식도 가능하다.

auto func = [pw = std::make_unique<Widget>()]
  { return pw->a() && pw->b(); };

이러한 초기화 갈무리를 일반화된 람다 갈무리(generalized lambda capture) 라고 부르기도 한다.

 

물론 람다로 할 수 있는 모든 것은 클래스 형태로 만들 수 있다. 위와 같은 동작을 하는 클래스 버전 코드를 보자.

class ClassVersion {
public:

  using DataType = std::unique_ptr<Widget>;

  explicit ClassVersion(DataType&& ptr)
  : pw(std::move(ptr)) {}

  bool operator()() const
  { return (Widget->a() && Widget->b()); }

private:
  DataType pw;
};

auto func = ClassVersion(std::make_unique<Widget>());

 

하지만 코드량이 많으므로, C++11 에서 이동 갈무리를 흉내내는 방법을 알아보도록 하자. 방법은 다음과 같다.

  1. 갈무리할 객체를 std::bind 가 산출하는 함수 객체로 이동하고,
  2. 그 '갈무리된' 객체에 대한 참조를 람다에 넘겨준다.

C++14 와 C++11 각각의 예시를 보자.

std::vector<double> data;

// C++14 버전
auto func = [data = std::move(data)]
  { /* 여기서 data 를 사용 */ }

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

std::bind 가 돌려주는 객체를 바인드 객체라고 부른다. 바인드 객체는 std::bind 에 전달된 모든 인수의 복사본들을 포함한다. 각 왼값 인수에 대해, 바인드 객체에는 그에 해당하는 복사 생성된 객체가 있고, 각 오른값에 대해서는 이동 생성된 객체가 있다. 위 예에서 둘째 인수는 오른값(std::move 의 결과이므로), data 는 바인드 객체 안으로 이동된다.

기본적으로, 람다로부터 만들어진 클로저 클래스의 operator() 멤버 함수는 const 이다. 이 때문에 람다 본문 안에서 클로저의 모든 자료 멤버는 const 가 된다. 그러나 바인드 객체 안의 이동 생성된 data 복사본은 const 가 아니다. 람다 안에서 data 복사본이 수정되지 않게 하려면 지금 예제에서처럼 람다의 매개변수를 const 에 대한 참조로 선언해야 한다. 변이 가능한 람다를 사용할 때는 mutable 을 붙여 주면 되며, 이 때는 매개변수 선언에서 cosnt 를 제거해야 한다.

auto func =
  std::bind(
    [](std::vector<double>& data) mutable
    { /* 여기서 data 를 사용 */ },
    std::move(data)
  );

바인드 객체는 std::bind 에 전달된 모든 인수의 복사본을 저장하므로, 위 예의 바인드 객체는 람다가 산출한 클로저(std::bind 의 첫 인수) 의 복사본도 저장한다. 따라서 그 클로저의 수명은 바인드 객체의 수명과 같다. 이는 클로저가 존재하는 한 이동 갈무리를 흉내내는 바인드 객체도 존재함을 뜻한다는 점에서 중요하다.

물론 항목 34에서 다루겠지만, std::bind 보다는 람다를 선호하는 것이 좋다. 하지만 C++11 에서는 std::bind 가 유용한 경우도 존재한다는 것만 알아두자!

 

Comments