KoreanFoodie's Study
Effective Modern C++ | 항목 32 : 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라 본문
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) 을 이용하면 된다. 초기화 갈무르리로는 다음과 같은 것들을 지정할 수 있다.
- 람다로부터 생성되는 클로저 클래스에 속한 자료 멤버의 이름
- 그 자료 멤버를 초기화하는 표현식
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 에서 이동 갈무리를 흉내내는 방법을 알아보도록 하자. 방법은 다음과 같다.
- 갈무리할 객체를 std::bind 가 산출하는 함수 객체로 이동하고,
- 그 '갈무리된' 객체에 대한 참조를 람다에 넘겨준다.
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 가 유용한 경우도 존재한다는 것만 알아두자!
'Tutorials > C++ : Advanced' 카테고리의 다른 글
Effective Modern C++ | 항목 34 : std::bind 보다 람다를 선호하라 (0) | 2022.10.26 |
---|---|
Effective Modern C++ | 항목 33 : std::forward 를 통해 전달할 auto&& 매개변수에는 decltype 을 사용하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 31 : 기본 갈무리 모드를 피하라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 30 : 완벽 전달이 실패하는 경우들을 잘 알아두라 (0) | 2022.10.26 |
Effective Modern C++ | 항목 29 : 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라 (0) | 2022.10.26 |