본문으로 건너뛰기 C++ decltype | Extract Expression Types

C++ decltype | Extract Expression Types

C++ decltype | Extract Expression Types

이 글의 핵심

decltype vs auto, decltype(auto), trailing return types, SFINAE with decltype, and the decltype(x) vs decltype((x)) pitfall for templates.

What is decltype?

decltype yields the type of an expression. Unlike plain auto in many cases, it can preserve top-level const and reference.

// 변수 선언 및 초기화
int x = 10;
decltype(x) y = 20;        // int
const int& ref = x;
decltype(ref) z = x;       // const int&

Key properties:

  • Unevaluated context (expression not executed)
  • Preserves cv-qualifiers and references
  • Different rules for id-expressions vs general expressions

auto vs decltype

The following example demonstrates the concept in cpp:

const int& r = x;
auto a = r;           // int (decay: removes const and reference)
decltype(r) b = r;    // const int& (preserves everything)
int arr[5];
auto a2 = arr;        // int* (decay to pointer)
decltype(arr) a3;     // int[5] (preserves array type)

Comparison table:

Featureautodecltype
Removes constYesNo
Removes referenceYesNo
Array decayYesNo
Function decayYesNo

Trailing return types (C++11)

Before C++14, auto with trailing return was needed for dependent types:

// 실행 예제
template<typename T, typename U>
auto add(T a, U b) -> decltype(a + b) {
    return a + b;
}
// Usage
auto result = add(1, 2.5);  // double

C++14 improvement: Plain auto with return type deduction:

template<typename T, typename U>
auto add(T a, U b) {
    return a + b;  // Type deduced automatically
}

decltype(auto) (C++14)

Combines auto deduction with decltype rules:

int& getRef();
const int& getConstRef();
auto a = getRef();            // int (decay)
decltype(auto) b = getRef();  // int& (exact type)
auto c = getConstRef();            // int (decay)
decltype(auto) d = getConstRef();  // const int& (exact type)

Perfect return type forwarding

template<typename Func, typename... Args>
decltype(auto) call(Func&& f, Args&&... args) {
    // Returns exact type from f, including references
    return std::forward<Func>(f)(std::forward<Args>(args)...);
}
int& getInt();
decltype(auto) result = call(getInt);  // int&, not int

SFINAE with decltype

Detecting member functions

#include <type_traits>
template<typename T>
auto process(T v) -> decltype(v.size(), void()) {
    std::cout << "Has size(): " << v.size() << "\n";
}
template<typename T>
void process(...) {
    std::cout << "No size()\n";
}
// Usage
std::vector<int> vec = {1, 2, 3};
process(vec);  // "Has size(): 3"
process(42);   // "No size()"

Using std::declval

#include <utility>
template<typename T>
auto getValue(T& container) 
    -> decltype(std::declval<T>()[0]) {
    return container[0];
}
// Works with any type that supports operator[]
std::vector<int> vec = {10, 20};
auto val = getValue(vec);  // int&

decltype(x) vs decltype((x))

Critical difference:

int x = 10;
decltype(x) a;      // int (id-expression: declared type)
decltype((x)) b = x; // int& (expression: lvalue reference)

Rules

ExpressionCategorydecltype result
x (id-expression)-Declared type
(x) (parenthesized)lvalueT&
std::move(x)xvalueT&&
42prvalueT

Real-world pitfall

template<typename T>
decltype(auto) forward(T& x) {
    return (x);  // ❌ Returns reference! Dangling if x is local
}
template<typename T>
decltype(auto) forward(T& x) {
    return x;  // ✅ Returns by value
}

Real-world applications

1. Generic lambda return type

auto lambda = [](auto x, auto y) -> decltype(x + y) {
    return x + y;
};
auto result1 = lambda(1, 2);      // int
auto result2 = lambda(1.5, 2);    // double
auto result3 = lambda(std::string("a"), std::string("b"));  // std::string

2. CRTP base class

template<typename Derived>
class Base {
public:
    decltype(auto) interface() {
        return static_cast<Derived*>(this)->implementation();
    }
};
class Derived : public Base<Derived> {
public:
    int& implementation() {
        static int value = 42;
        return value;
    }
};
Derived d;
int& ref = d.interface();  // Returns int&, not int

3. Perfect proxy

template<typename T>
class Proxy {
    T* ptr_;
    
public:
    Proxy(T* p) : ptr_(p) {}
    
    template<typename... Args>
    decltype(auto) operator()(Args&&... args) {
        // Forwards exact return type from T::operator()
        return (*ptr_)(std::forward<Args>(args)...);
    }
};

Common mistakes

Mistake 1: Dangling reference with decltype(auto)

decltype(auto) getLocal() {
    int x = 10;
    return (x);  // ❌ Returns int&, but x is destroyed!
}
// ✅ Fix: remove parentheses
decltype(auto) getLocal() {
    int x = 10;
    return x;  // Returns int (copy)
}

Mistake 2: Unnecessary decltype in variable declarations

int x = 10;
decltype(x) y = 20;  // ❌ Verbose, just use int
// ✅ Better
int y = 20;
// decltype useful when type is complex
std::vector<int> vec;
decltype(vec.begin()) it = vec.begin();  // ✅ Good use case

Mistake 3: Confusing decltype(auto) with auto&&

int x = 10;
auto&& a = x;              // int& (forwarding reference)
decltype(auto) b = x;      // int (copy)
decltype(auto) c = (x);    // int& (reference)

Type inspection at compile time

template<typename T>
struct TD;  // Type Displayer (intentionally undefined)
int x = 10;
TD<decltype(x)> xType;      // Error shows: TD<int>
TD<decltype((x))> xRefType; // Error shows: TD<int&>

Runtime type info

#include <typeinfo>
#include <iostream>
int x = 10;
std::cout << typeid(decltype(x)).name() << "\n";      // "i" (int)
std::cout << typeid(decltype((x))).name() << "\n";    // "i" (reference removed)
// Better: use boost::core::demangle or compiler-specific

Performance implications

Zero overhead: decltype is purely compile-time. No runtime cost.

// Both produce identical assembly
int x = 10;
int y = x;              // Direct
decltype(x) z = x;      // Via decltype

Advanced: decltype in SFINAE

Detecting operator overloads

template<typename T, typename U>
auto canAdd(int) -> decltype(std::declval<T>() + std::declval<U>(), std::true_type{});
template<typename T, typename U>
std::false_type canAdd(...);
// Usage
static_assert(decltype(canAdd<int, int>(0))::value);  // true
static_assert(!decltype(canAdd<int, std::string>(0))::value);  // false

Expression validity

template<typename T>
struct has_size {
private:
    template<typename U>
    static auto test(int) -> decltype(std::declval<U>().size(), std::true_type{});
    
    template<typename>
    static std::false_type test(...);
    
public:
    static constexpr bool value = decltype(test<T>(0))::value;
};
static_assert(has_size<std::vector<int>>::value);
static_assert(!has_size<int>::value);

C++20 improvements

Concepts replace many decltype SFINAE patterns

// C++11-17: decltype SFINAE
template<typename T>
auto process(T v) -> decltype(v.size(), void());
// C++20: Concepts
template<typename T>
    requires requires(T v) { v.size(); }
void process(T v);

Compiler support

Compilerdecltypedecltype(auto)Notes
GCC4.3+4.9+Full support
Clang2.9+3.3+Excellent diagnostics
MSVC2010+2015+Some early bugs

Keywords

C++, decltype, decltype(auto), trailing return type, templates, type deduction, SFINAE, C++11, C++14


Frequently Asked Questions (FAQ)

Q. When would I use this in practice?

A. decltype vs auto, decltype(auto), trailing return types, SFINAE with decltype, and the decltype(x) vs decltype((x)) pitf…

Q. What should I read before this?

A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.

Q. Where can I study this more deeply?

A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers C++, decltype, type-deduction, C++11.