본문 바로가기
C++/Effective Modern C++

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

by COCO1337 2020. 8. 6.

C++11에서는 이동 전용 객체들(std::unique_ptr, std::future 등)을 클로저 안으로 들여올 수가 없다. 또한 복사는 비싸고 이동은 저렴한 객체를 클로저 안으로 들여온다면 복사보다는 이동이 적용되는 것이 바람직한데 역시 불가능하다.

C++14에서는 객체를 클로저 안으로 이동하는 수단을 직접 제공한다. 이동 갈무리를 위해 새로운 갈무리 메커니즘을 도입했는데 그것을 초기화 갈무리 라고 한다.

초기화 갈무리는 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름과 그 자료 멤버를 초기화하는 표현식을 지정할 수 있다.

다음은 초기화 갈무리의 한 예이다.

class Widget{
public:
  ...
  bool isValidated() const;
  bool isProcessed() const;
  bool isArchived() const;
  
private:
  ...
};

auto pw = std::make_unique<Widget>();	// Widget 생성, 이후에 *pw 조작
...
auto func = [pw = std::move(pw)]	// 클로저의 자료멤버를 std::move(pw)로 초기화
        { return pw->isValidated() && pw->isArchived(); };

pw = std::move(pw) 에서 "="의 좌변은 클로저 클래스 안의 자료멤버의 이름이고, 우변은 그것을 초기화하는 표현식이다. 좌변의 범위는 해당 클로저 클래스의 범위이고, 우변의 범위는 람다가 정의되는 지점의 범위와 동일하다(람다 이전에 선언된 객체).

즉, "pw = std::move(pw)"는 클로저 안에서 자료 멤버 pw를 생성하되, 지역 변수 pw에 std::move를 적용한 결과로 그 자료멤버를 초기화 하라는 뜻이다.

만약 *pw를 조작할 필요가 없다면 이런식으로 일반화 시킬 수 있다.

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

C++14의 갈무리 개념이 C++11에 비해 훨씬 일반화 되었고, 그래서 초기화 갈무리를 일반화된 람다 갈무리 라고 부르기도 한다.

 

물론 람다 표현식은 컴파일러가 하나의 클래스를 자동으로 작성해서 그 클래스의 객체를 생성하게 하는 수단이기 때문에 람다 표현식으로 표현할 수 있다면 C++11에서 직접 만들어서 수행하는것이 가능하다. 하지만 C++11에서 초기화 갈무리를 흉내내는 방법이 있다.

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

그러면 std::vector를 이용해 C++14와 C++11을 비교해보자

std::vector<double> data;
...
// C++14
auto func = [data = std::move(data)] { ... };

// C++11, 초기화 갈무리를 흉내내기
auto func = std::bind(
  [](const std::vector<double>& data)
  { ... },
  std::move(data)
);

std::bind는 람다 표현식처럼 함수 객체를 산출한다. 이 때 std::bind가 돌려주는 함수 객체를 바인드 객체라고 하자. 바인드 객체는 std::bind에 전달된 모든 인수의 복사본들을 포함한다. 각 왼값 인수에 대해 바인드 객체에는 그에 해당한 복사 생성된 객체가 있다. 이 예에서 둘째 인수는 std::move의 결과로서 바인드 객체 안으로 이동한다. 바인드 객체가 호출되면 바인드 객체에 저장된 인수들이 std::bind 호출시 첫 인수로 지정한 호출 가능 객체에 전달된다.

C++11에서 사용한 람다는  C++14와 같지만 이동 갈무리 흉내를 내기 위한 객체에 해당하는 data 매개변수가 추가되었다는 차이가 있다. 이 매개변수는 바인드 객체 안의 data 복사본에 대한 왼값 참조이다.(std::move(data) 자체는 오른값이지만 data의 복사본 자체는 왼값) 따라서 람다 본문 안에서는 이동 생성된 data 복사본을 사용하게 된다.

 

기본적으로 람다로부터 만들어진 클로저 클래스의 operator() 멤버 함수는 const이다. 하지만 바인드 객체 안의 이동생성된 data 복사본은 const가 아니다. 람다 안에서 복사본이 수정되지 않게 하려면 const에 대한 참조로 선언해야 한다. 변이 가능한 람다를 사용한다면 mutable로 선언하여 const를 제거한다.

// C++11에서 mutable 람다의 초기화 갈무리 흉내내기
auto func = std::bind(
  [](std::vector<double>& data) mutable
  { ... }
  std::move(data)
);

바인드 객체는 std::bind에 전달된 모든 인수의 복사본을 저장하기 때문에 람다가 산출한 클로저의 복사본도 저장한다. 그러므로 그 클로저의 수명은 바인드 객체의 수명과 같다.

std::bind를 사용하는 것에 대한 요점은

1. 객체를 C++11 클로저 안으로 이동 생성하는것은 불가능하지만 객체를 C++11 바인드 객체 안으로 이동 생성하는 것은 가능하다.

2. C++11에서 이동 갈무리를 흉내내는 방법은 객체를 바인드 객체 안으로 이동 생성하고, 이동 생성된 객체를 람다에 참조로 전달하는 것이다.

3. 바인드 객체의 수명이 클로저의 수명과 같으므로 바인드 객체 안의 객체들을 마치 클로저 안에 있는것처럼 취급할 수 있다.


Conclusion

- 객체를 클로저 안으로 이동할 때에는 C++14의 초기화 갈무리를 사용하라

- C++11에서는 직접 작성한 클래스나 std::bind로 초기화 갈무리를 흉내낼 수 있다


Reference

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

반응형

댓글