[Modern C++] 항목 33: std::forward를 통해서 전달할 auto&& 매개변수에는 decltype을 사용하라

게시:     수정

카테고리:

태그: ,

이 글은 아래의 책을 정리하였습니다.
이펙티브 모던 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에 완벽 전달해야 한다. 코드에서 바꿀 점은 두 가지다.
    1. x가 보편 참조(항목 24)여야 한다.
    2. 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 참조 타입이 된다.
  • 항목 28에서는 std::forward 호출 시 전달할 인수가 lvalue임을 나타내기 위해서는 lvalue 참조 타입 인수를 사용하고, rvalue임을 나타내기 위해서는 비참조 타입 인수를 사용하는 것이 관례라는 점도 설명했다.
    • x가 lvalue에 묶였다면 decltype(x)는 lvalue 참조가 된다. 이는 관례에 맞는다.
    • 그러나 x가 rvalue에 묶였다면 decltype(x)는 rvalue 참조가 되는데, 이는 관례(비참조)와는 맞지 않는다.

🔍 rvalue 참조 타입을 지정해도 괜찮은 이유

  • 항목 28의 std::forward C++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을 사용하라.

Cpp 카테고리 내 다른 글 보러가기

댓글남기기