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:
| Feature | auto | decltype |
|---|---|---|
| Removes const | Yes | No |
| Removes reference | Yes | No |
| Array decay | Yes | No |
| Function decay | Yes | No |
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
| Expression | Category | decltype result |
|---|---|---|
x (id-expression) | - | Declared type |
(x) (parenthesized) | lvalue | T& |
std::move(x) | xvalue | T&& |
42 | prvalue | T |
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
Print type with error
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
| Compiler | decltype | decltype(auto) | Notes |
|---|---|---|---|
| GCC | 4.3+ | 4.9+ | Full support |
| Clang | 2.9+ | 3.3+ | Excellent diagnostics |
| MSVC | 2010+ | 2015+ | Some early bugs |
Related posts
Keywords
C++, decltype, decltype(auto), trailing return type, templates, type deduction, SFINAE, C++11, C++14
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. decltype vs auto, decltype(auto), trailing return types, SFINAE with decltype, and the decltype(x) vs decltype((x)) pitf… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ auto 키워드 | ‘타입 추론’ 가이드
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
- C++ Reference Collapsing | ‘레퍼런스 축약’ 가이드
이 글에서 다루는 키워드 (관련 검색어)
C++, decltype, type-deduction, C++11 등으로 검색하시면 이 글이 도움이 됩니다.