decltype 지정자

decltype 지정자 (C++11)

decltype은 엔티티(entity)의 선언된 타입(declared type)을 조사해요. 표현식이라면 그 표현식의 타입과 값 범주(value category)까지 함께 알려주죠. 우리가 손으로 쓰기 어렵거나 아예 못 쓰는 타입, 그러니까 람다 관련 타입이나 템플릿 매개변수에 의존하는 타입을 다룰 때 특히 유용한 키워드예요.

이 절의 목표는 decltype이 어떤 규칙으로 타입을 뽑아내는지를 정확히 이해하는 거예요. 공식 언어 레퍼런스 수준으로 규칙을 짚어볼게요.

출처: cppreference

본문

문법 (Syntax)

decltype은 인자 하나를 괄호로 감싸서 써요.

표기 의미
decltype(entity) (1) 엔티티의 선언된 타입을 조사
decltype(expression) (2) 표현식의 타입과 값 범주를 조사

설명 (Explanation)

먼저 인자가 괄호로 묶지 않은 id-expression인 경우예요. 여기서 id-expression이라는 건 이름을 가리키는 표현식을 뜻해요. 괄호 없이 이름을 그대로 넘기면 decltype은 그 이름이 가리키는 엔티티의 타입을 돌려줘요. 다만 그런 엔티티가 없거나, 인자가 오버로드된 함수들의 집합을 가리키면 프로그램은 ill-formed(형식에 어긋남)가 돼요.

버전에 따라 괄호 없는 id-expression이 여러 가지 특별한 대상을 가리킬 때가 있어요.

  • 구조적 바인딩(structured binding)을 가리키는 경우 (C++17부터): decltype은 그 구조적 바인딩의 참조형(referenced type) 을 돌려줘요. 이 타입은 구조적 바인딩 선언의 명세에 설명돼 있어요.
  • 비타입 템플릿 매개변수(non-type template parameter)를 가리키는 경우 (C++20부터): 템플릿 매개변수의 타입을 돌려줘요. 매개변수가 자리표시자 타입(placeholder type)으로 선언됐다면 필요한 타입 추론을 먼저 수행해요. 그리고 엔티티가 템플릿 매개변수 객체(그 자체는 const 객체)라도, 돌려주는 타입은 const가 아니에요.
  • 연결(splice) 표현식을 가리키는 경우 (C++26부터): 그 표현식이 가리키는 엔티티·객체·값의 타입을 돌려줘요.

그 다음이 인자가 그 밖의 다른 표현식인 경우예요. 표현식의 타입을 T라고 할 때, 값 범주에 따라 이렇게 나뉘어요.

  • a) expression의 값 범주가 xvalue이면 decltypeT&&를 돌려줘요.
  • b) 값 범주가 lvalue이면 decltypeT&를 돌려줘요.
  • c) 값 범주가 prvalue이면 decltypeT를 돌려줘요.

여기서 임시 객체가 만들어지는지가 버전에 따라 달라져요.

  • (C++17 이전) expression클래스 타입의 prvalue를 돌려주는 함수 호출이거나, 그런 함수 호출을 오른쪽 피연산자로 두는 콤마 표현식이라면, 그 prvalue에 대해 임시 객체가 생기지 않아요.
  • (C++17부터) expression이 (괄호로 묶였을 수 있는) 즉시 호출(immediate invocation)을 제외한 prvalue라면, 그 prvalue에서 임시 객체가 materialize되지 않아요. 즉 그런 prvalue에는 결과 객체(result object)가 없어요.

꼭 기억해 둘 점이 하나 있어요. 객체의 이름을 괄호로 감싸면 보통의 lvalue 표현식으로 취급돼요. 그래서 decltype(x)decltype((x))는 서로 다른 타입이 되는 경우가 아주 흔해요.

decltype은 앞서 말했듯 표준 표기로는 선언하기 어렵거나 불가능한 타입을 선언할 때 유용해요. 대표적으로 람다 관련 타입이나 템플릿 매개변수에 의존하는 타입이 그렇죠.

참고 (Notes)

decltype의 존재를 감지하는 기능 검사 매크로(feature-test macro)는 다음과 같아요.

기능 검사 매크로 표준 기능
__cpp_decltype 200707L (C++11) decltype

키워드 (Keywords)

decltype은 C++의 키워드예요.

예제 (Example)

decltype의 규칙을 코드로 직접 확인해 볼게요. 여기서는 두 종류의 핵심 규칙, 즉 선언된 타입lvalue 표현식의 타입이 어떻게 달라지는지를 봐요.

#include <cassert>
#include <iostream>
#include <type_traits>

struct A { double x; };
const A* a;

decltype(a->x) y;       // y의 타입은 double (선언된 타입)
decltype((a->x)) z = y; // z의 타입은 const double& (lvalue 표현식)

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) // 반환 타입이 템플릿 매개변수에 의존
                                      // C++14부터는 반환 타입 추론이 가능
{
    return t + u;
}

const int& getRef(const int* p) { return *p; }
static_assert(std::is_same_v<decltype(getRef), const int&(const int*)>);
auto getRefFwdBad(const int* p) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdBad), int(const int*)>,
    "Just returning auto isn't perfect forwarding.");
decltype(auto) getRefFwdGood(const int* p) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdGood), const int&(const int*)>,
    "Returning decltype(auto) perfectly forwards the return type.");

// 이렇게 써도 됩니다:
auto getRefFwdGood1(const int* p) -> decltype(getRef(p)) { return getRef(p); }
static_assert(std::is_same_v<decltype(getRefFwdGood1), const int&(const int*)>,
    "Returning decltype(return expression) also perfectly forwards the return type.");

int main()
{
    int i = 33;
    decltype(i) j = i * 2;
    static_assert(std::is_same_v<decltype(i), decltype(j)>);
    assert(i == 33 && 66 == j);

    auto f = [i](int av, int bv) -> int { return av * bv + i; };
    auto h = [i](int av, int bv) -> int { return av * bv + i; };
    static_assert(!std::is_same_v<decltype(f), decltype(h)>,
        "The type of a lambda function is unique and unnamed");

    decltype(f) g = f;
    std::cout << f(3, 3) << ' ' << g(3, 3) << '\n';
}

출력:

42 42

예제에서 가장 인상적인 부분은 getRef를 돌려보내는 두 함수예요. auto로만 돌려주면 반환 타입이 int로 떨어져서 완벽 전달(perfect forwarding)이 되지 않고, decltype(auto)decltype(반환 표현식)을 쓰면 const int&가 그대로 보존돼요. 반환 타입의 자격(const 등)까지 그대로 전달하고 싶다면 decltype이 필요하다는 걸 보여주는 장면이에요.

더 알아보기 (Learn more)

  • auto 지정자 (C++11) — 표현식에서 타입을 추론해요. decltype과 함께 반환 타입 추론, decltype(auto)의 근간이 돼요. (cppreference)
  • declval (C++11) — 평가되지 않는 문맥(unevaluated context)에서 사용하기 위해 템플릿 타입 인자의 객체에 대한 참조를 얻는 함수 템플릿이에요. (cppreference)
  • is_same (C++11) — 두 타입이 같은지 검사하는 클래스 템플릿이에요. decltype의 결과를 검증할 때 자주 같이 써요. (cppreference)
  • underlying_type (C++11) — 주어진 열거형의 내부 정수 타입을 얻는 클래스 템플릿이에요. (cppreference)