[Modern C++] 항목 33: std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라
카테고리: Cpp
이 글은 아래의 책을 정리하였습니다.
이펙티브 모던 C++, 스콧 마이어스 저자, 류광 번역
가독성이 떨어지는 직역들을 수정하며 정리하였습니다.
e.g. 연역 → 추론, 중복적재 → 오버로딩
📦 6. 람다 표현식
👉🏻 항목 33: std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라
🔍 일반적 람다(generic lambda)
- C++14에서 가장 고무적인 기능은 일반적 람다, 즉 매개변수 명세에
auto를 사용하는 람다다. 구현은 간단하다. 람다의 클로저 클래스의operator()를 템플릿 함수로 만들면 된다.
auto f = [](auto x){ return normalize(x); };
- 이 람다가 생성하는 클로저 클래스의 함수 호출 연산자는 다음과 같은 모습이다.
class 컴파일러가_만든_어떤_클래스_이름 {
public:
template<typename T>
auto operator()(T x) const // auto 반환 타입은 항목 3을 보라
{ return normalize(x); }
… // 클로저 클래스의 나머지 기능들
};
🔍 문제: lvalue만 전달된다
- 이 람다는 매개변수
x를normalize에 전달할 뿐이다. 만일normalize가 lvalue와 rvalue를 다르게 처리한다면 이 람다는 제대로 작성한 것이 아니다.normalize에 항상 lvalue(매개변수x)를 전달하기 때문이다. 인수가 rvalue였다면 rvalue를 전달해야 마땅하다. - 제대로 하려면 람다가
x를normalize에 완벽 전달해야 한다. 코드에서 바꿀 점은 두 가지다.x가 보편 참조(항목 24)여야 한다.x를std::forward(항목 25)를 통해normalize에 전달해야 한다.
auto f = [](auto&& x)
{ return normalize(std::forward<???>(x)); };
- 개념적으로는 자명하지만,
std::forward에 지정할 타입(???)을 무엇으로 해야 할까?
🔍 해결: decltype(x)
- 보통의 완벽 전달은 타입 매개변수
T를 받는 템플릿 함수 안에서std::forward<T>라고 하면 그만이다. 그러나 일반적 람다에는 그런 식으로 사용할 타입 매개변수T가 없다. 람다가 생성하는 클로저 클래스의 템플릿operator()에는T가 있지만 람다에서 그T를 가리킬 수는 없으므로 무용지물이다. - 항목 28에서 설명했듯이, 보편 참조 매개변수에 lvalue 인수를 넘겨주면 그 매개변수의 타입은 lvalue 참조가 되고, rvalue를 넘겨주면 rvalue 참조가 된다. 따라서 람다에 주어진 인수가 lvalue인지 rvalue인지는 매개변수
x의 타입을 조사해 보면 알 수 있다. 그 수단이 바로decltype(항목 3 참고)이다.- lvalue가 전달되었다면
decltype(x)는 lvalue 참조에 해당하는 타입이 된다. - rvalue가 전달되었다면
decltype(x)는 rvalue 참조 타입이 된다.
- lvalue가 전달되었다면
- 항목 28에서는
std::forward호출 시 전달할 인수가 lvalue임을 나타내기 위해서는 lvalue 참조 타입 인수를 사용하고, rvalue임을 나타내기 위해서는 비참조 타입 인수를 사용하는 것이 관례라는 점도 설명했다.x가 lvalue에 묶였다면decltype(x)는 lvalue 참조가 된다. 이는 관례에 맞는다.- 그러나
x가 rvalue에 묶였다면decltype(x)는 rvalue 참조가 되는데, 이는 관례(비참조)와는 맞지 않는다.
🔍 rvalue 참조 타입을 지정해도 괜찮은 이유
- 항목 28의
std::forwardC++14 구현을 다시 보자.
template<typename T> // 이름공간
T&& forward(remove_reference_t<T>& param) // std 안에서
{
return static_cast<T&&>(param);
}
- 일반적으로 클라이언트 코드가
Widget타입의 rvalue를 완벽 전달할 때에는T를Widget(비참조)으로std::forward를 인스턴스화한다.
Widget&& forward(Widget& param) // T가 Widget일 때의
{ // std::forward의
return static_cast<Widget&&>(param); // 인스턴스화 결과
}
- 그런데 클라이언트 코드가 같은 rvalue를 완벽 전달하되,
T를 비참조 타입으로 지정하는 관례를 따르지 않고 rvalue 참조 타입(Widget&&)으로 지정하면 어떻게 될까? 다음은std::forward가 인스턴스화되고std::remove_reference_t가 적용된 후, 그러나 참조 축약(항목 28)이 적용되기 전의 인스턴스 모습이다.
Widget&& && forward(Widget& param) // T가 Widget&&일 때의
{ // std::forward의
return static_cast<Widget&& &&>(param); // 인스턴스화 결과
} // (참조 축약은 아직 적용되지 않았음)
- rvalue 참조에 대한 rvalue 참조는 단일한 rvalue 참조가 된다는 참조 축약 규칙을 적용하면 다음과 같다.
Widget&& forward(Widget& param) // T가 Widget&&일 때의
{ // std::forward의
return static_cast<Widget&&>(param); // 인스턴스화 결과
} // (참조 축약이 적용된 이후)
- 이 인스턴스는
std::forward<Widget>인스턴스, 즉T를Widget으로 지정했을 때의 인스턴스와 동일하다. 즉, rvalue 참조 타입으로std::forward를 인스턴스화한 결과는 비참조 타입으로 인스턴스화한 결과와 같다. - 따라서 람다의 매개변수
x에 rvalue가 전달되면decltype(x)가 rvalue 참조 타입이 되더라도, 관례적 타입(비참조)을 사용했을 때와 같은 결과가 나온다. 결국 lvalue이든 rvalue이든decltype(x)를std::forward로 넘겨주면 우리가 원하는 결과가 된다.
🔍 완벽 전달 람다
auto f = [](auto&& x)
{
return
normalize(std::forward<decltype(x)>(x));
};
- 마침표 세 개(
...)를 두 번 추가하면 매개변수 하나가 아니라 임의의 개수의 매개변수를 받아서 완벽하게 전달하는 람다가 된다. 이는 C++14의 람다가 가변 인수를 지원하는 덕분이다.
auto f = [](auto&&... xs)
{
return
normalize(std::forward<decltype(xs)>(xs)...);
};
🧐 정리
std::forward를 통해서 전달할auto&&매개변수에는decltype을 사용하라.
댓글남기기