템플릿과 auto의 타입 추론에서 일어나는 일과 달리 decltype은 주어진 이름이나 표현식의 구체적인 타입을 그대로 말해준다.

 

const int i = 0; // decltype(i) = const int
bool f(const Widget& w) // decltype(w) = const Widget&, decltype(f) = bool(const Widget&)
struct Point { int x, y; }; // decltype(Point::x) = int, decltype(Point::y) = int
Widget w; // decltype(w) = Widget
if (f(w)) ... // decltype(f(w)) = bool
std::vector<int> v; // decltype(v) = vector<int>
if(v[0] == 0) ... // decltype(v[0]) = int&

전부 다 직관적이고 당연한 타입이다.

 

decltype은 함수의 반환 타입이 매개변수 타입들에 의존하는 함수 템플릿을 선언할 때 주로 사용된다.

이게 무슨 소리인고 하니, 예를 들어서 컨테이너와 인덱스를 매개변수로 받아서 사용자 인증 후에 컨테이너의 operator[]를 호출해서 특정 요소를 반환해주는 함수 템플릿을 작성한다고 해보자.

 

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

대체로 컨테이너들의 operator[]는 T&를 반환하지만 vector<bool>에 대해서만은 예외이다. (Effective STL 항목 18 참고)

아무튼 컨테이너에 따라 반환 타입이 다를수도 있다는 점이다.

 

위와 같이 후행 반환 타입 구문이 쓰인 함수에서 auto는 반환값의 타입 추론과 아무런 연관이 없다. 그저 반환 타입을 매개변수 목록 다음에 선언하겠다는 점을 나타낼 뿐이다. 단, C++14는 후행 반환 타입을 생략하더라도 타입 추론이 된다.

그런데 함수의 반환 타입이 auto라면(C++14부터 가능) 템플릿 타입 추론 규칙을 따른다는것을 항목 2 마지막에 언급한것을 기억하는가?

 

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

위에서 언급한대로 컨테이너의 operator[]는 대체로 T& 타입을 반환해준다.

그런데 템플릿 타입 추론 규칙이 적용되면 참조성이 무시되므로 반환 타입은 T가 되어버린다.

 

std::deque<int> d;
...
authAndAccess(d, 5) = 10; // 오른값 = 오른값

왼값에 오른값을 대입하는것을 기대했지만 오른값이 반환되기 때문에 컴파일이 되지 않는다.

그럼 어떻게 해야하는가? 후행 반환 타입을 꼭 써주어야 하나? 아니다. 방법이 있다.

 

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

신기한 문법같아 보이지만 하나씩 따져보면 어렵지 않다.

반환 타입이 auto라는것은 타입을 추론해야 한다는 것을 의미하고, 그것을 decltype으로 감쌌다는것은 타입 추론 과정에서 decltype 타입 추론 규칙들이 적용되어야 한다는 뜻이다. 단, C++14 이상에서 작동한다. C++11은 후행 반환 타입을 꼭 명시해야 한다.

c[i]의 타입은 T&이기 때문에 decltype 타입 추론 규칙에 따르면 T&가 되어서 후행 반환 타입을 명시하지 않아도 된다.

특히나 vector<bool>같이 operator[]의 반환 타입이 T&가 아닌 경우에도 같은 타입의 객체를 반환해준다.

 

decltype(auto)를 함수 반환 타입에만 사용할 수 있는것은 아니다.

 

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

myWidget1은 템플릿 타입 추론 Case3를 따라서 참조성과 const가 무시되어 값의 복사가 이루어졌다.

하지만 myWidget2는 decltype 타입 추론 규칙에 의해 cw의 타입으로 추론된다.

 

 

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i)

마무리하기 전에 함수의 매개변수를 살펴볼 필요가 있다. c의 타입은 비const 객체에 대한 왼값 참조이다.

그런데 만약 c의 인수로 오른값이 오게되면 문제가 발생할 수 있다.

그래서 매개변수가 왼값 뿐만 아니라 오른값도 받을 수 있게 선언을 수정해야 한다. 오버로딩을 사용해도 되지만 관리해야 하는 함수가 늘어나기 때문에 좋은 선택이 아니다.

 

해결 방법은 항목 1의 Case2에서 잠시 언급되었던 보편 참조이다.

 

template<typename Container, typename Index>
decltype(auto) authAndAccess(Container&& c, Index i) // Case2
/* C++11, auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i]) */
{
    authenticateUser();
    return std::forward<Container>(c)[i]; // 전달받은 c의 참조 타입을 그대로 반환
}

 

마지막으로 decltype이 제공하는 몇가지 특별한 경우가 있다. 그 중 한가지만 언급한다.

 

int x = 0;

decltype(x); // x = int
decltype((x)); // (x) = int&

 

x를 괄호로 한번 감쌌을 뿐인데 타입이 다르다. decltype을 이름에 적용하면 그 이름에 대해 선언된 타입이 산출되지만, 이름보다 복잡한 왼값 표현식에 대해서는 일반적으로 항상 왼값 참조를 보고한다.

(x)는 이름보다 복잡한 표현식이라고 판단하는 것이다.

 

decltype(auto) f1()
{
    ...
    return x; // int
}

decltype(auto) f2()
{
    ...
    return (x); // int&
}

C++11에서는 드물게 만나는 신기한 현상 정도로 치부할 수 있지만 C++14에서는 return문 작성 습관때문에 결과 자체가 달라질 수도 있다는 것을 주의해야 한다.

 

◾ decltype은 항상 변수나 표현식의 타입을 아무 수정 없이 보고한다.

◾ decltype은 타입이 T이고 이름이 아닌 왼값 표현식에 대해서는 항상 T& 타입을 보고한다.

◾ C++14는 decltype(auto)를 지원한다. decltype(auto)는 auto처럼 초기화 값으로 타입을 추론하지만, 그 타입 추론 과정에서 decltype의 규칙들을 적용한다.

+ Recent posts