티스토리 뷰
람다식의 관련된 문제로 여기서 말하는 갈무리는 캡쳐절을 주의하라는 의미이다. 아래 링크를 참고하자.
기본 갈무리 모드는 두가지로 나눌 수 있다. 하나는 참조에 의한 갈무리 모드와 값에 의한 갈무리 모드를 의미한다. 일반적으로 참조에 의한 갈무리 모드가 위험하다는 것을 알지만 값에 의한 갈무리 모드가 위험하다는 것을 인지하지 못한다. 두가지 방법이 어떤식으로 문제를 야기하는지 알아보도록 하자.
참조에서의 문제
참조에서 발생하는 문제는 간단하다. 지역 변수를 참조로 캡쳐한 상황에서 클로저보다 지역 변수가 먼저 소멸되는 경우 참조를 잃게 되는 문제이다. 코드로 살펴보자.
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 는 addDivisorFilter 함수의 지역변수이다. 문제는 람다 함수가 실행되는 시점보다 함수가 먼저 끝나버리기 때문에 divisor 는 소멸되어 버린다. 람다 함수가 실행되는 시점에는 소멸된 변수에 접근하게 되어 문제가 발생하게 된다. 이 문제는 기본 참조 갈무리가 아닌 명시적으로 지정해도 문제가 발생한다.
filters.emplace_back(
[&divisor](int value)
{ return value % divisor == 0; }
);
클로저를 즉시 사용하는 것이 가장 깔끔하게 해결 할 수 있다.
template<typename C>
void workWithContainer(const C& container)
{
auto calc1 = computeSomeValue1();
auto calc2 = computeSomeValue2();
auto divisor = computeDivisor(calc1, calc2);
using std::begin;
using std::end;
if (std::all_of(
begin(container), end(container),
[&](const auto& value)
{ return value % divisor == 0; })
) {
...
} else {
...
}
}
위 코드가 안전하지만 다른 코드에서는 안전성이 깨질 수 있다. 참조 대신 값을 사용하면 어떨까?
기본 값에서의 문제
아래 코드로 앞서 말한 문제를 해결 할 수 있다.
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
);
기본 값 갈무리 모드를 사용하면 위 문제는 안전하기는 하다. 그러나 예상만큼 안전하지는 않다. 아래의 코드를 살펴보자.
class Widget {
public:
...
void addFilter() const;
private:
int divisor;
};
void Widget::addFilter() const
{
filters.emplace_back(
[=](int value) { return value % divisor == 0; }
};
}
위 코드를 살펴봤을때는 마치 문제가 없는 것처럼 보인다. 아래의 코드를 살펴보자.
void Widget::addFilter() const
{
filters.emplace_back(
[](int value) { return value % divisor == 0; } // 컴파일 오류
);
}
void Widget::addFilter() const
{
filters.emplace_back(
[divisor](int value) // 컴파일 오류
{ return value % divisor == 0; }
);
}
위 코드에서 컴파일 오류가 발생하는 이유는 무엇일까?
그 이유는 캡쳐에 사용할 수 있는 범위는 static 이 아닌 지역 변수에만 적용되기 때문이다. divisor 는 지역 변수가 아니기 때문에 문제가 발생하는 것이다.
그렇다면 기본 갈무리 모드에서 문제가 없었던 이유는 무엇일까?
바로 this 포인터 때문이다. 복사가 되는 값은 바로 this 이기 때문이다. 그럼 똑같은 문제가 발생한다. 클로저가 사용되기 전에 this 포인터가 소멸될 수 있기 때문이다. 다행히도 해결 방법은 간단하다. divisor 를 지역 변수로 복사하면 된다.
void Widget::addFilter() const
{
auto divisorCopy = divisor;
filters.emplace_back(
[divisorCopy](int value)
{ return value % divisorCopy == 0; }
);
}
// C++14
void Widget::addFilter() const
{
filters.emplace_back(
[divisor = divisor](int value)
{ return value % divisorCopy == 0; }
);
}
위의 방법으로 기본 값 갈무리도 사용하지 않고 this 포인터가 소멸되도 문제가 생기지 않게 되었다. 마지막으로 static 을 사용하는 경우를 살펴보자.
void addDivisorFilter()
{
static auto calc1 = computeSomeValue1();
static auto calc2 = computeSomeValue2();
static auto divisor =
computeDivisor(calc1, calc2);
filters.emplace_back(
[=](int value)
{ return value % divisor == 0; }
);
++divisor;
}
위 람다식에서 사용하는 divisor 는 복사본이라고 생각할 수도 있겠다. 그러나 앞서 말했듯이 static 이 아닌 지역변수만 캡쳐가 가능하기 때문에 위에서 사용하는 divisor 는 복사본이 아니다. 여기서 사용하는 divisor 는 static 으로 선언된 divisor 이기 때문이다.
기본 참조 갈무리와 기본 값 갈무리는 문제를 발생시킬 여지를 만든다. 그러므로 기본 갈무리는 피하는 것이 좋겠다.
참고 서적
스콧 마이어스, Effective Modern C++
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] 항목 33. std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라 (0) | 2020.01.18 |
---|---|
[Effective Modern C++] 항목 32. 객체를 클로저 안으로 이동하려면 초기화 갈무리를 사용하라 (0) | 2020.01.12 |
[Effective Modern C++] 항목 30. 완벽 전달이 실패하는 경우들을 잘 알아두라. (0) | 2019.12.15 |
[Effective Modern C++] 항목 29. 이동 연산이 존재하지 않고, 저렴하지 않고, 적용되지 않는다고 가정하라 (0) | 2019.12.14 |
[Effective Modern C++] 항목 28. 참조 축약을 숙지하라 (0) | 2019.12.08 |
- Total
- Today
- Yesterday
- std::move
- async
- C++14
- C++
- thread
- 포인터
- auto
- 보편 참조
- Effective
- MOVE
- C++11
- Join
- 보편참조
- const
- CPP
- C
- detach
- 람다
- Modern
- Overloading
- forward
- Override
- 다이소
- Unreal
- Forwarding
- Perfect
- Effective Modern C++
- 발아시기
- Future
- std::forward
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |