참조 캡쳐를 사용하는 경우에 클로저가 지역 변수나 매개변수의 수명보다 오래 지속되면 클로저 안의 참조는 대상을 잃게된다.
using FilterContainer = std::vector<std::function<bool(int)>>;
FilterContainer filters;
void addDivisorFilter() {
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
filters.emplace_back([&](int value) { return value % divisor == 0; });
} // divisor 소멸
지역변수 divisor를 참조하여 클로저를 filters에 추가해주었는데 addDivisorFilter가 반환되면 divisor 역시 소멸하기 때문에 미정의 행동을 유발하게 된다.
별도로 저장하지 않고 해당 함수 내에서 즉시 사용하고 클로저가 소멸된다면 참조가 대상을 잃는 일은 발생하지 않는다.
그렇다고 하더라도 다른 사람이 코드가 유용해보여서 복사해서 가져다 쓰는 경우에는 참조를 잃는 일이 발생할 가능성이 다시 생긴다. 명시적인 캡쳐가 아니라 아닌 기본 캡쳐이기 때문에 않기 때문에 divisor의 수명을 한눈에 알아보기 어렵기 때문이다.
이를 해결하는 한 가지 방법은 기본 캡쳐모드를 참조(&)가 아닌 값(=)으로 사용하는 것이다.
그런데 또 완벽하지도 않은게 값으로 캡쳐한 대상이 포인터라면 외부에서 삭제할 가능성이 존재하기 때문이다.
class Widget {
public:
void addFilter() const;
private:
int divisor;
};
void Widget::addFilter() const {
filters.emplace_back([=](int value) { return value % divisor == 0; });
}
우선은 각설하고 다시 본론으로 돌아와서 이번에는 참조가 아닌 값으로 캡쳐한다.
divisor의 값이 클로저 안으로 복사되기 때문에 안전하지 않을까? 싶겠지만 전혀 아니다.
캡쳐는 오직 람다가 생성된 범위 안에서 보이는 static이 아닌 지역 변수에만 적용된다. divisor는 멤버 변수이기 때문에 캡쳐가 되지 않는다. 기본 캡쳐이든 명시적 캡쳐이든 전혀 상관이 없다. 오히려 기본 캡쳐를 빼버리면 컴파일 에러가 발생한다.
캡쳐도 되지않고 그렇다고 캡쳐를 빼버리면 컴파일 에러가 발생하는데 그 이유는 컴파일러가 클로저 안의 divisor를 this->divisor로 취급하기 때문이다.
분명히 값으로 캡쳐했지만 내부적으로 this포인터로 참조하기 때문에 divisor는 객체의 수명에 의해 제한되어버린다.
해결법은 없을까?
void Widget::adFilter() const {
auto divisorCopy = divisor;
filters.emplace_back([divisorCopy](int value){ return value % divisorCopy == 0; });
// [divisor = divisor](int value){ return value % divisorCopy == 0; } C++14
// divisor를 클로저에 복사
}
의외로 간단하다. 지역 복사본을 만들어서 그걸 넘겨주면 되는 것이다. C++14는 초기화 캡쳐가 가능하다.
기본 값 캡쳐 모드는 클로저 밖에서 일어나는 자료의 변화로부터 격리되어 있을거라는 오해를 부를 수 있는데 전혀 아니다. 전역 또는 이름공간에서 정의된 클래스나 객체, static으로 선언된 객체는 캡쳐 모드와 무관하게 클로저 안에서도 사용할 수 있다.
◾ 기본 참조 캡쳐모드는 참조가 대상을 잃을 위험이 있다.
◾ 기본 값 캡쳐모드는 포인터(특히 this)가 대상을 잃을 수 있으며, 람다가 자기 완결적이라는 오해를 부를 수 있다.
'도서 > Effective Modern C++' 카테고리의 다른 글
[6장] 항목 33 : std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라 (0) | 2023.01.27 |
---|---|
[6장] 항목 32 : 객체를 클로저 안으로 이동하려면 초기화 캡쳐를 사용하라 (0) | 2023.01.27 |
[5장] 항목 30 : 완벽 전달이 실패하는 경우들을 잘 알아두라 (0) | 2023.01.27 |
[5장] 항목 29 : 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라 (0) | 2023.01.25 |
[5장] 항목 28 : 참조 축약을 숙지하라 (0) | 2023.01.25 |