Item 3. decltype을 이해해라


decltype

template이나 auto는 상황에 따라 타입 추론 규칙이 다르게 적용되지만 decltype은 변수나 표현식의 선언된 타입(declared type)을 그대로 반환한다.

const int i = 0;
// decltype(i) => const int

bool f(const int n);
// decltype(n) => const int 
// decltype(f) => bool(const int)
// decltype(f(1)) => bool

struct Point {
  int x, y;
};
// decltype(Point::x) => int
// decltype(Point::y) => int

Widget w;
// decltype(w) => Widget

decltype은 매개변수 타입에 따라 리턴 값의 타입이 바뀌는 함수 템플릿을 만들 때 사용한다.

아래의 예시는 Container와 Index를 parameter로 받아서 작업을 수행하고 [](인덱스 연산자)를 통해 Container의 원소를 반환하는 템플릿 함수이다.

template <typename Container, typename Index>
auto authAndAccess(Container& c, Index i) {
  authenticateUser();
  return c[i];
}

C++11에서는 auto가 Container의 원소 타입을 추론할 수 없다.

C++11에서는 한 문장으로 구성된 람다식(single-statement lambda)에 대해서만 타입 추론을 허용하기 때문이다.

따라서 C++11에서 리턴 타입을 추론하기 위해서는 trailing return type과 decltype을 사용해야 한다.


후행 반환 타입(Trailing Return Type)

Trailing return type은 함수의 반환 타입을 매개변수 목록 다음에 선언하는 구문이다. (-> 뒤에 선언)

template <typename Container, typename Index>
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) {
  authenticateUser();
  return c[i];
}

반환 타입을 함수 이름 앞이 아닌 뒤에 선언하기 때문에 매개변수인 c, idecltype을 통해 리턴 타입을 추론할 수 있다.

이때 함수 앞의 auto는 타입 추론을 하는 것이 아니라 뒤에서 trailing return type을 사용한다는 것을 의미한다.

C++14부터는 trailing return type 없이 auto만 써도 리턴 타입 추론이 가능하다.

template <typename Container, typename Index>
auto authAndAccess(Container& c, Index i) {
    authenticateUser();
    return c[i];
}

하지만 Item 2에서 말했듯이 리턴 타입에서 사용하는 autotemplate의 타입 추론 규칙을 따른다.

이 규칙으로 인해 문제가 발생한다.

std::vector<int> v;
authAndAccess(v,5) = 10; // 컴파일 오류, rvalue인 int에 10을 대입

c[i]가 어떤 T 타입의 reference를 리턴할 경우, auto의 타입 추론에 의해 이 함수의 리턴 타입은 T&가 아닌 T가 된다. (template 타입 추론에서 arugment들의 reference-ness를 무시함)

이 문제를 해결하고자 decltype(auto)를 사용한다.


decltype(auto)

C++14에서 지원하는 decltype(auto)는 타입 추론을 하되 그 규칙은 decltype의 규칙을 따른다는 의미이다.

Widget w;
const Widget& cw = w;
 
auto myWidget1 = cw;           // auto => Widget
decltype(auto) myWidget2 = cw; // decltype(auto) => const Widget&

decltype(auto)를 사용하면 문제 없이 리턴 타입을 추론할 수 있다.

template <typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) {
  authenticateUser();
  return c[i];
}

std::vector<int> v;
authAndAccess(v,5) = 10; // 이제 c[i]의 타입 그대로 int&를 리턴

하지만 만약 함수에 rvalue 컨테이너를 전달해야 한다면 또 다른 문제가 발생할 수 있다.

template <typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i) {
  authenticateUser();
  return c[i];
}

std::deque<std::string> makeStringDeque(); // 팩토리 함수

// makeStringDeque 함수가 리턴한 deque 임시 객체를 authAndAccess 함수에 넘겨줌
auto s = authAndAccess(makeStringDeque(), 5); // 컴파일 오류

makeStringDeque 함수는 호출 문장 끝에서 파괴되는 임시 객체를 authAndAccess 함수에 넘겨준다.

c[i]는 임시 객체 내부의 한 요소를 가리키고 있으므로, 문장이 끝나면 s는 허상 포인터(dangling pointer)가 된다.

이 문제를 해결하기 위해 Container가 lvalue이면 참조하고 rvalue이면 복사할 수 있게 universal referencestd::forward를 사용한다.

//C++11에서는 decltype(auto)를 쓸 수 없으니 trailing return type 사용
template <typename Container, typename Index>
auto authAndAccess(Container&& c, Index i) -> decltype(std::forward(Container>(c)[i]) {
  authenticateUser();
  return std::forward<Container>(c)[i];
}

//C++ 14
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) {
  authenticateUser();
  return std::forward<Container>(c)[i]; // lvalue, rvalue에 맞게 반환
}


decltype(auto)의 예외 상황

decltype은 이름에 선언된 타입을 그대로 반환하지만 이름보다 복잡한 표현식을 사용할 경우 예외 상황이 발생한다.

decltype을 lvalue 표현식에 적용하면 lvalue referece 타입을 반환한다.

decltype(auto) f1() {
  int x = 0;
  return x;    // decltype(x)는 int
}

decltype(auto) f2() {
  int x = 0;
  return (x);  // decltype((x))는 int&, x는 이름이지만 (x)는 표현식이기 때문 
}

x(x)는 모두 int 타입으로 추론될 것 같지만 (x)는 예상과 다르게 int&로 추론된다.

f2 함수는 f1 함수와 리턴 타입이 다를 뿐만 아니라 스택 프레임이 파괴되면서 없어질 지역 변수의 reference를 반환하는 문제가 있다.

따라서 decltype(auto)를 표현식과 사용할 때는 많은 주의가 필요하다.


카테고리:

업데이트: