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
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.
Related Articles (Internal Links)
Other articles related to this topic.
- C++ auto 키워드 | ‘타입 추론’ 가이드
- C++ auto 타입 추론 | 복잡한 타입을 컴파일러에 맡기기
- C++ Reference Collapsing | ‘레퍼런스 축약’ 가이드
Keywords Covered in This Article (Related Search Terms)
This article covers C++, decltype, type-deduction, C++11.