728x90
반응형

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

template<typename T> // std::vector
class vector
{
public:
    ...
    T& operator[](std::size_t index);
    ...
};

vector<int> v; // decltype(v)은 vector<int>
...
if (v[0] == 0) ... // decltype(v[0])은 int&

 

C++11에서는 decltype은 함수의 반환 타입이 그 매개변수 타입에 의존하는 함수 템플릿을 선언할 때 주로 쓰인다.

예를 들어 컨테이너 하나와 색인 하나를 받고, 우선 사용자를 인증한 후 operator []를 통해서 컨테이너의 한 요소를 돌려주는 함수를 작성한다고 하자.

그러면 함수 반환 타입은 반드시 그러한 operator[]의 반환 타입과 동일해야 한다. 즉, 컨테이너의 operator []의 반환 타입이 컨테이너에 따라 다를 수 있다.

이런 함수의 반환 타입은 decltype을 이용하면 손쉽게 표현할 수 있다.

template <typename Container, typename Index> // 작동하지만, 좀 더 다듬을 필요가 있다.
auto authAndAccess(Container& c, Index i) -> decltype(c[i]) 
{
    authenticateUser();
    return c[i];
}

이름 앞에 auto를 지정하는 것은 타입 추론과는 아무런 관련이 없고, 이 auto는 C++11의 후행 반환 타입 (trailing return type) 구문이 쓰인다는 것을 나타낼 뿐이다.
후행 반환 타입 구문은, 함수의 반환 타입을 매개변수 목록 다음에("->" 다음 위치에) 선언하는 구문을 말한다.
만일 통상적인 방식으로 함수 이름 앞에서 반환 타입을 지정한다면, 아직 선언되지 않았으므로 c와 i는 사용할 수 없다.

 

C++11은 람다 함수가 한 문장으로 이루어져 있다면 그 반환 타입의 추론을 허용하며, C++14는 허용 범위를 더욱 확장해서 모든 람다와 모든 함수의 반환 타입 추론을 허용한다.(심지어 return문이 여러 개인 함수도 허용한다. 단, 모든 return 문의 타입 추론 결과가 일치해야 한다.)

 

따라서, authAndAccess의 경우 C++14에서는 후행 반환 타입을 생각하고 그냥 함수 이름 앞의 auto만 남겨두어도 된다.

이런 형태의 선언 에서는 실제로 auto가 타입 추론이 일어남을 뜻하는 용도로 쓰이며, 이 경우 컴파일러가 함수의 구현으로부터 함수의 반환 타입을 추론한다.

 

항목 2에서 설명했듯이, 함수의 반환 타입에 auto가 지정되어 있으면 컴파일러는 템플릿 타입 추론을 적용하는데, 대부분의 operator []는 T&를 반환하지만, 항목 1은 템플릿 타입 추론 동안 초기화 표현식의 참조성이 무시된다고 설명했기 때문에 위의 예에서는 문제가 된다.

 

이러한 문제를 해결하기 위해 C++14에서는 decltype(auto) 지정자가 도입되었다.

auto는 해당 타입이 추론되어야 함을 뜻하고, decltype은 그 추론 과정에서 decltype 타입 추론 규칙들이 적용되어야 함을 뜻한다.

template <typename Container, typename Index> // C++14 아주 정확한 것은 아님
auto authAndAccess(Container& c, Index i) 
{ 
    authenticateUser();
    return c[i]; // 반환 타입은 c[i]로부터 추론된다.
}
 
// T객체들을 담은 컨테이너에 대한 operator[] 연산은 대부분의 경우에는 T&를 돌려준다.
// 문제는, 항목 1에서 설명했듯이 템플릿 타입 추론 과정에서 초기화 표현식의 참조성이 무시된다는 점이다.

std::deque<int> d;
...
authAndAccess(d, 5) = 10; // Error! 사용자를 인증하고, d[5]를 돌려주고, 그런 다음 10을 d[5]에 대입한다. 이 코드는 컴파일되지 않는다!
 
// 여기서 d[5]는 int&를 돌려주나, authAndAccess에 대한 auto 반환 타입 추론 과정에서 참조가 제거되기 때문에 결국 반환 타입은 int가 된다.
// 이런 문제를 해결하기 위해 decltype(auto) 지정자를 사용할 수 있다.
 
template <typename Container, typename Index> // C++14, 작동하지만 좀 더 다듬을 필요가 있음.
decltype(auto) 
authAndAccess(Container& c, Index i) 
{ 
    authenticateUser();
    return c[i];
}
 
// 이제 authAndAccess의 반환 타입은 실제로 c[i]의 반환 타입과 일치한다.

---
// decltype(auto)를 함수 반환 타입에만 사용할 수 있는 것은 아니다. 
// 변수를 선언할 때에도, 초기화 표현식에 decltype타입 추론 규칙들을 적용하고 싶은 경우라면 이 지정자가 유용하다.
 
Widget w; 
const Widget& cw = w; 

auto myWidget1 = cw; // auto 타입 추론, myWidget1의 타입은 Widget
 
decltype(auto) myWidget2 = cw; // decltype 타입 추론, myWidget2의 타입은 const Widget&
 
---
// 앞에서 언급하고 설명하지 않은 authAndAccess을 다듬는 방법에 대해서 알아보자. 
// authAndAccess 함수의 C++14 버전의 선언을 다시 보면,
 
template <typename Container, typename Index>
decltype(auto) authAndAccess(Container& c, Index i);
 
// 컨테이너 c는 비 const 객체에 대한 좌측값 참조로서 함수에 전달된다.
// 이는 함수가 돌려준 컨테이너 요소를 클라이언트(함수를 호출한 쪽)가 수정할 수 있게 하기 위한 것이다.
// 문제는, 이 때문에 함수에 우측값 컨테이너는 전달할 수 없다는 것이다. 
// 우측값을 좌측값 참조에 묶을 수는 없다.(const에 대한 좌측값 참조에는 묶을 수 있지만, 지금은 그런 경우가 아니다).
// 우측값도 넘길 수 있게 하려면, 항목 24에서 설명하겠지만 보편 참조(universal reference)를 사용해야 한다.
 
template <typename Container, typename Index> // 이번에는 c가 보편 참조(universal reference)
decltype(auto) authAndAccess(Container&& c, Index i);
 
// 선언에 맞게 구현도 고칠 필요가 있다.
// 구체적으로, 항목 25의 조언에 따라 다음과 같이 보편 참조(universal reference)에 std::forward를 적용하기로 한다.
 
template <typename Container, typename Index> // 최종 C++14 버전
decltype(auto) authAndAccess(Container&& c, Index i) 
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

---
// 만일 C++14를 지원하는 컴파일러가 없다면 템플릿의 C++11 버전을 사용할 수밖에 없다.

// 반환 타입을 명시적으로 지정해야 한다는 점만 빼고는 C++14 버전과 동일하다.
 
template <typename Container, typename Index> // 최종 C++11 버전
auto authAndAccess(Container&& c, Index i) -> decltype(std::forward<Container>(c)[i])
{
    authenticateUser();
    return std::forward<Container>(c)[i];
}

 

decltype을 이름에 적용하면 그 이름에 대해 선언된 타입이 산출된다. 대체로 이름은 좌측값 표현식이나, 그 점은 decltype의 행동에 영향을 주지 않는다.

그런데 이름보다 복잡한 좌측값 표현식에 대해서는 일반적으로 decltype이 항상 좌측값 참조를 반환한다. 즉, 이름이 아닌 그리고 타입이 T인 어떤 좌측값 표현식에 대해 decltype은 T&를 반환한다.

어차피 대부분의 좌측값 표현식에는 태생적으로 좌측값 참조가 포함되어 있으므로, 이 점 때문에 뭔가가 달라지는 경우는 드물다. 하지만 이러한 작동 방식이 뭔가 차이를 만드는 경우가 있긴 하다.

아래의 코드를 보자.

int x = 0;

x는 변수의 이름이므로 decltype(x)는 int이다. 그러나 x를 괄호로 감싸서 "(x)"를 만들면 이름보다 복잡한 표현식이 된다. 이름으로서의 x는 하나의 좌측값이며, C++은 (x)라는 표현식도 좌측값으로 정의한다.

따라서 decltype((x))는 int&이다. 이름을 괄호로 감싸면 decltype이 보고하는 타입이 달라지는 것이다.

C++11에서는 이것이 그냥 드물게 만나는 신기한 현상 정도이다. 그렇지만 decltype(auto)를 지원하는 C++14에서는 return 문 작성 습관의 사소한 차이 때문에 함수의 반환 타입 추론 결과가 달라지는 사태가 벌어질 수 있다.

decltype(auto) f1(void)
{
    int x = 0;
    ...
    return x; // decltype(x)는 int이므로 f1은 int를 반환
}
 
decltype(auto) f2(void)
{
    int x = 0;
    ...
    return (x); // decltype((x))는 int&이므로 f2는 int&를 반환
}
 
// f2가 f1과는 다른 타입을 돌려준다는 점뿐만 아니라, 자신의 지역 변수에 대한 참조를 돌려준다는 점도 주목하기 바란다.
// 이런 종류의 코드를 작성하는 것은 미정의 행동으로 직행하는 특급 열차에 올라타는 것과 같다.

이 예의 주된 교훈은, decltype(auto)는 아주 조심해서 사용해야 한다는 것이다. 애초에 예상했던 타입이 실제로 추론되었는지 확인하고 싶다면 항목 4에서 설명하는 기법들을 사용하면 된다.

그렇지만, 보통의 경우 decltype은 기대한 바로 그 타입을 산출한다. 특히 decltype을 이름에 적용할 때에는 반드시 그렇다. 이 경우 decltype은 주어진 이름에 선언된 타입(declared type)을 반환한다.

요약

  • decltype은 항상 변수나 표현식의 타입을 아무 수정 없이 반환한다.
  • decltype은 타입이 T이고 이름이 아닌 좌측값 표현식에 대해서는 항상 T&타입을 반환한다.
  • C++14는 decltype(auto)를 지원한다. decltype(auto)는 auto처럼 초기치으로부터 타입을 추론하지만, 그 타입 추론 과정에서 decltype의 규칙들을 적용한다.
728x90
반응형