-
[Effective Modern C++] Lambda : auto 매개변수C++/Effective Modern C++ 2022. 9. 25. 13:30
C++14의 주요 기능 중 하나는, 람다 매개변수에 auto를 사용할 수 있게 된 것입니다.
이 기능의 구현은 람다의 클로저 클래스의 수정에 있습니다.
auto f = [](auto x){ return normalize(x); }
다음과 같은 람다가 있을 때, 이 람다가 만드는 클로저 클래스의 operator()연산자는 다음과 같이 됩니다.
class CLASS_GENERATED_BY_COMPILER { public: template<typename T> auto operator()(T x) const { return normalize(x); } ... };
이 코드는, 호출하는 함수에 따라 문제가 발생할 여지가 있습니다.
예를 들어 위 예제에서 호출한 normalize함수가 R-value와 L-value를 다른 방식으로 처리한다면, 이 람다는 비정상적으로 작동할 수 있습니다.
이번 글에서는 위와 같은 문제에 대하여 살펴보도록 하겠습니다.
1. 문제에 대한 이해
더보기서론의 구현에서, 람다는 normalize에 항상 L-value를 전달합니다.
만약 서론의 가정대로 normalize가 R-value와 L-value의 처리방식이 다르다면, 주어진 인수가 R-value일 때 R-value를 전달해야 합니다.
간단하게 요약해서, 람다는 normalize에 x를 완벽전달 해야합니다.
그렇게 되기 위해서, x는 보편 참조이어야 하고, x를 전달할 때 std::forward를 사용해야 합니다.
코드를 수정할 경우 개념적으로는 아래와 같습니다.
auto f = [](auto&& x){ return normalize(std::forward<???>(x)); }
개념적으로 라는 말을 덧붙인 이유는, std::forward의 템플릿 인수가 문제이기 때문입니다.
템플릿 함수의 경우 형식 매개변수 T를 사용할 수 있지만, 람다에서는 그렇지 못합니다.
위와 같이 템플릿 형식을 사용할 수 없을 때 사용할 수 있는 수단은 decltype()이 있습니다.
하지만 decltype도 표면적으로는 문제가 발생합니다.
다음 문단에서 이어집니다.
2. std::forward와 decltype
더보기std::forward호출 시 전달할 인수가 L-value임을 나타내기 위해서는 L-value참조 형식 인수를 사용하고, R-value형식을 나타내기 위해서는 비 참조 형식 인수를 사용하는 것이 관례입니다.
하지만 이번 경우 (decltype을 사용한 경우)에는 이것이 충족되지 않습니다.
- x가 L-value일 경우, decltype(x)는 L-value참조를 산출합니다.
- x가 R-value일 경우, decltype(x)는 R-value참조를 산출합니다.
std::forward에 대하여 R-value는 비 참조 형식을 사용해야 하는데, 이는 관례와 맞지 않습니다.
3. 증명
더보기std::forward의 C++14 구현을 살펴보도록 하겠습니다.
template<typename T> T&& forward(remove_reference_t<T>& param) { return static_cast<T&&>(param); }
위 구현에 대하여, 클라이언트가 MyClass라는 형식의 R-value를 완벽하게 전달할 때에는 MyClass형식으로 std::forward가 인스턴스화 할 것입니다.
그 때의 std::forward 템플릿은 다음과 같이 인스턴스화 합니다.
MyClass&& forward(MyClass& param) { return static_cast<MyClass&&>(param); }
여기서, 클라이언트 코드가 MyClass형식의 동일한 R-value를 완벽 전달하고, T를 비참조 형식으로 지정하는 관례를 따르지 않은 채 R-value참조 형식으로 지정할 경우를 생각해보도록 하겠습니다.
요약하자면 T를 MyClass&&로 지정하는 경우이고, 이번 글의 decltype의 경우입니다.
MyClass&& && forward(MyClass& param) { return static_cast<MyClass&& &&>(param); }
이 문맥에는 참조 축약이 적용될 수 있고, 최종적으로는 다음과 같은 모습이 됩니다.
MyClass&& forward(MyClass& param) { return static_cast<MyClass&&>(param); }
이 인스턴스와 std::forward<MyClass> 인스턴스 (= T가 MyClass일 경우)를 비교해볼 경우, 동일하게 연역됩니다.
즉, R-value참조 형식으로 std::forward를 인스턴스화 한 결과는 비참조 형식으로 인스턴스화 한 결과와 같습니다.
4. 결론
더보기람다에 R-value가 전달되었을 때 decltype(x)가 산출하는 형식이 관례와 맞지 않더라도 (비참조가 아니더라도) 관례적 형식을 사용했을 때와 동일한 결과가 도출됩니다.
따라서, R-value와 L-value모두 decltype(x)를 std::forward에 전달할 경우 완벽 전달이 성립됩니다.
완벽 전달을 이용해서 보완한 서론의 코드는 아래와 같이 완성될 수 있습니다.
// Before auto f = [](auto x){ return normalize(x); } // After auto f = [](auto&& x){ return normalize(std::forward<decltype(x)>(x)); }
결론은 std::forward를 통해 auto&&매개변수를 전달할 때는 std::forward를 사용하는 것이 바람직하다 입니다.
본문의 문제 상황 파악, 해결에 사용된 std::forward, 보편 참조와 같은 내용은 다른 글에 기술되어 있습니다.
- R-value reference와 Universal reference
- std::move와 std::forward [1] [2]
- 참조 축약
감사합니다.
'C++ > Effective Modern C++' 카테고리의 다른 글
[Effective Modern C++] std::thraed와 std::async (0) 2023.03.23 [Effective Modern C++] Lambda와 std::bind (0) 2022.10.03 [Effective Modern C++] Lambda : Init capture (0) 2022.09.20 [Effective Modern C++] Lambda : Default capture의 위험성 (0) 2022.08.30 [Effective Modern C++] 완벽 전달(Perfect forwarding)의 실패 (0) 2022.08.23