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>());
- 람다로 이동 캡쳐를 흉내내고 싶다면?
- 캡쳐할 객체를 std::bind가 리턴하는 함수 객체로 이동시키고,
- 그 캡쳐된 객체에 대한 참조를 람다의 매개변수로 넘긴다.
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를 붙여줘야 한다.
- 람다의 클로저 클래스의 operator() 멤버 함수는 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