4. decltype 키워드

decltype키워드는 주어진 표현식의 타입을 컴파일러가 직접 추론해서 결정하라고 지시하는데 사용한다.. 컴파일러에게 타입을 정하게 한다는 말은 auto 키워드와 비슷하나, decltype 키워드는 주로 함수의 반환타입 결정에 사용한다.

decltype은 declared type(선언된 형식)의 줄임말로써, 주어진 표현식의 타입을 알려주는 키워드이다. TR1부터 도입된 decltype은 주로 주어진 템플릿 인자에 기반한 generic programming의 어려움을 해소하기 위해 도입되었다.

auto가 값에 상응하는 타입을 추론시켜주는 키워드라면,
decaltype은 값으로부터 타입을 추출해 낼 수 있는 키워드라고 생각하면 된다.
두 키워드 모두 컴파일 타임에 동작한다.

int int add1(int a, int b) {
    typedef int TYPE_ADD_1;
    TYPE_ADD_1 c = a + b;
    return c;
}
int add2(int a, int b) {
    typedef decltype(a+b) TYPE_ADD_2;
    TYPE_ADD_2 c = a + b;
    return c;
}
auto add3(int a, int b) {
    return a + b;   // 컴파일러가 반환타입을 알 수 없어 Error!!!
}
auto add4(int a, int b) -> int {
    return a + b;
}

전달되는 파라미터와 상관없이 반환 타입이 정해지는 상황인 경우

#include <string>
#include <iostream>
using namespace std;

// auto와 decltype 키워드를 이용한 템플릿 함수
template<typename T, typename F>
auto execute(const T& value, F func) -> decltype(func(value)) {
    return func(value);
}

// 정수 타입 숫자를 해당 문자로 변경하는 함수
string number2String(const int i) {
    switch(i) {
        case 1:
            return "one";
        case 2:
            return "two";
        case 3:
            return "three";
        case 4:
            return "four";
        default:
            return "unknown";
    }
}

// 문자 타입 숫자를 해당 정수로 변경하는 함수
int string2Number(const string& str) {
    if(!str.compare("one"))
        return 1;
    else if(!str.compare("two"))
        return 2;
    else if(!str.compare("three"))
        return 3;
    else if(!str.compare("four"))
        return 4;
    else
        return -1;
}


int main(int argc, char** argv) 
{
    // 
    cout <<execute(3, number2String) <<endl;
    cout <<execute("three", string2Number) <<endl;

    // 
    cout << "execute(3, number2String) has return type :\n"
        <<typeid(execute(3, number2String)).name() <<endl;
    cout << "execute(\"three\", string2Number has return type :\n" 
        <<typeid(execute("three", string2Number)).name() <<endl;
    return 0;
}

decltype 키워드는 명시적인 타입 지정 상황에서는 그다지 효용이 없다. 하지만 템플릿 함수를 동반하는 제너릭generic 코드를 작성할 때는 기존에 굉장히 어렵거나 구현이 거의 불가능했던 로직을 간단하게 구현할 수 있다. 이것이 decltype 키워드가 새롭게 추가된 이유이다.

C++11(VS2013)부터 auto 타입 반환이 가능해지고 trailing return type(후행 반환 형식)으로 decltype을 사용함으로써, 템플릿 함수의 auto 반환이 상당히 유연해지고 자유로워졌다.

// auto 반환함수와 후행 반환 형식으로 int 사용
auto add_function(int a, int b) -> int
{
    return a + b;
}

// auto 반환과 후행 반환 형식으로 decltype()을 사용
template <typename T, typename U>
auto add_template(T&& x, U&& y) -> decltype(std::forward<T>(x) + std::forward<U>(y))
{
    return std::forward<T>(x) + std::forward<U>(y);    
}

// BUILDER의 makeObject() 반환 형식으로부터 자유로워짐
template <typename TBuilder>
auto MakeAndProcessObject(const TBuilder& builder) -> decltype(builder.makeObject())
{
    auto val = builder.makeObject();
    // process...
    return val;
}

C++11에서 auto 반환 함수는 반드시 후행 반환 형식을 지정해 주어야 하며, 특히 템플릿 함수들의 경우 타입을 템플릿 인자들로부터 추론해야 하므로 decltype을 활용하지 않으면, 컴파일 단계에서 auto 반환 형식을 재대로 추론하지 못해 컴파일 에러가 발생하게 된다.

error: 'add_template' function uses 'auto' type specifier without trailing return type
 auto add_template(T x, U y)// -> decltype(x + y)

C++14(VS2015)부터는 auto 반환시 후행 반환 형식을 지정해 주지 않아도 문제 없이 반환 타입을 추론해 준다. 즉, C++11에서 그랬듯 일일히 후행 반환 형식을 써주지 않아도 되는 것이다.

#include <iostream>
#include <string>

template <typename T, typename U>  
auto add_template(T&& x, U&& y) // -> decltype(std::forward<T>(x) + std::forward<U>(y))
{
    return std::forward<T>(x) + std::forward<U>(y);    
}

auto add_function(int a, int b) // -> decltype(a+b)
{
    return a + b;
}

template <typename TBuilder>
auto MakeAndProcessObject(const TBuilder& builder)      // -> decltype(builder.makeObject())
{
    auto val = builder.makeObject();
    // process...
    return val;
}

struct Builder
{
    static std::string makeObject() { return std::string("hello"); }
};

int main()
{
    add_template(1, 2);
    add_function(1, 2);

    // a is std::string type
    Builder builder;
    auto a = MakeAndProcessObject(builder);
}