본문 바로가기

C++

[Basic] Type deduction (template, auto, decltype) 유의할 점

C++98에서는 형식 연역(Type deduction)에 관한 규칙이 template에 대한 것 밖에 없지만, C++11부터는 auto와 decltype에 관한 규칙이 추가되었다. 우리는 C++ 코드를 작성하면서 종종 auto 키워드를 사용하게 되는데 제대로 사용하지 못하면 예기치 못한 규칙으로 인해 에러가 발생할 수 있다. 사용 시에 유의할 점을 알아보자.

Template 형식에서의 유의할 점

함수의 Template 선언은 대부분 아래와 같은 형식이다.

template<typename T>
void f(ParamType param);

우리는 2가지 형식을 연역하게 되는데, 하나는 T와 ParamType에 관한 것이다. 이 두 형식은 다른 경우가 많다. ParamType은 const나 reference qualifier 같은 수식어 들이 붙기 때문이다.

template<typename T>
void f(const T& param);

이때 ParamType이 universal reference 일 때와 pass-by-value 상황일 때에 유의할 점을 알아보자.

ParamType이 reference 일때

  • T는 비참조로 연역된다.
template<typename T>
void f(T& param) {}   // param is a reference

int main()
{
  int x = 27;         // x is an int
  const int cx = x;   // cx is a const int
  const int& rx = x;  // rx is a reference to x as a const int

  f(x);               // T is int, param's type is int&

  f(cx);              // T is const int,
                      // param's type is const int&

  f(rx);              // T is const int,
                      // param's type is const int&
}

ParamType이 universal reference 일 때

  • Rvalue을 참조하고 있지만 연역된 형식은 Lvalue 참조이다. 정상적인 Rvalue라면 상관없다.
(글쓴이) T&&의 또 다른 의미는 오른 값 참조 또는 왼 값 참조 중 하나라는 것이다.
template<typename T>
void f(T&& param) {}      // param is now a universal reference

int main()
{
  int x = 27;             // as before
  const int cx = x;       // as before
  const int& rx = x;      // as before

  f(x);                   // x is lvalue, so T is int&,
                          // param's type is also int&

  f(cx);                  // cx is lvalue, so T is const int&,
                          // param's type is also const int&

  f(rx);                  // rx is lvalue, so T is const int&,
                          // param's type is also const int&

  f(27);                  // 27 is rvalue, so T is int,
                          // param's type is therefore int&&
}

ParamType이 pass-by-value 일 때

  • Argument가 참조 형식이라면 참조 부분이 무시된다. 만약 const 라면 const 역시 무시된다.
(글쓴이) 참조형식이 무시되는 점은 유의해야 한다 생각한다. 우리는 보통 값 복사 cost를 줄이기 위해 주로 참조형식으로 값을 넘긴다. 하지만 개발자의 실수로 인해 무의미해질 수도 있다.
template<typename T>
void f(T param) {}          // param is now passed by value

int main()
{
  int x = 27;               // as before
  const int cx = x;         // as before
  const int& rx = x;        // as before
  f(x);                     // T's and param's types are both int

  f(cx);                    // T's and param's types are again both int

  f(rx);                    // T's and param's types are still both int


  const char* const ptr =   // ptr is const pointer to const object
    "Fun with pointers";

  f(ptr);                   // pass arg of type const char * const
}

Auto 형식에서 유의할 점

auto 형식 연역은 template 형식 연역과 겹치는 부분이 많다. 한 가지 예외를 제외한다면 auto 형식 연역이 곧 template 형식 연역이 된다.

중괄호를 통한 변수 선언

  • std::initializer_list <T> 형식으로 변수를 선언하게 된다.
  • Template 함수에 동일하게 중괄호 초기치를 전달하면 컴파일 에러가 발생한다. (auto는 중괄호 초기치가 std::initializer_list를 나타낸다고 가정하지만 template 연역은 못한다.)
#include <initializer_list>

template<typename T>  // template with parameter
void f(T param) {}    // declaration equivalent to
                      // x's declaration

template<typename T>
void f2(std::initializer_list<T> initList) {}

int main()
{
  {
    int x1 = 27;
    int x2(27);
    int x3 = {27};
    int x4{27};
  }

  {
    auto x1 = 27;    // type is int, value is 27
    auto x2(27);     // ditto
    auto x3 = {27};  // type is std::initializer_list<int>,
                     // value is {27}
    auto x4{27};     // ditto

    //auto x5 = {1, 2, 3.0};  // error! can't deduce T for
    //                        // std::initializer_list<T>
  }

  {
    auto x = { 11, 23, 9 };  // x's type is
                             // std::initializer_list<int>

    //f({ 11, 23, 9 });        // error! can't deduce type for T

    f2({ 11, 23, 9 });        // T deduced as int, and initList's
                              // type is std::initializer_list<int>
  }
}

함수 반환 형식이 auto 일 때

  • auto 형식 연역이 아니라 template 형식 연역의 규칙이 적용된다. (중괄호 초기치 전달 X)
  • 반환이 Reference라면 참조가 제거되어 반환되게 된다. 예) int& 반환한다면 int가 반환된다. (이를 방지하기 위해 decltype을 사용한다.)
#include <vector>

auto createInitList()
{
  //return {1, 2, 3};    // error: can't deduce type
  //                     // for {1, 2, 3}
}

int main()
{
  std::vector<int> v;

  auto resetV =
    [&v](const auto& newValue) { v = newValue; };  // C++14

  //resetV( {1, 2, 3} );  // error! can't deduce type
  //                      // for { 1, 2, 3 }
}

decltype 사용 시 유의점

  • decltype은 항상 변수나 표현식의 형식을 아무 수정 없이 보고한다.
  • 소괄호로 x를 감싸 (x)가 되면 Lvalue가 되므로 T&를 반환한다.
decltype(auto) f1()
{
  int x = 0;
  // ...
  return x;  // decltype(x) is int, so f1 returns int
}

decltype(auto) f2()
{
  int x = 0;
  // ...
  return (x);  // decltype((x)) is int&, so f2 returns int&
}