How Decltype Inspects Types in C++

The decltype specifier in C++ inspects and extracts the exact declared type of an expression or variable at compile time without executing any code. This article explains how decltype operates, detailing the distinction between bare identifiers and general expressions, how value categories dictate reference generation, and how compile-time type deduction is resolved in practice.

The Unevaluated Context

decltype operates strictly at compile time. Like sizeof, the operand passed to decltype is an unevaluated operand. The compiler analyzes the abstract syntax tree and type attributes of the expression without generating runtime instructions for evaluation.

int get_value();
decltype(get_value()) x; // x is of type int; get_value() is never called.

Rule 1: Id-Expressions and Member Access

When the argument passed to decltype(e) is an unparenthesized id-expression (a simple variable name) or a class member access, decltype yields the exact declared type of that entity.

int a = 10;
const int& b = a;

decltype(a) var1; // Type: int
decltype(b) var2 = a; // Type: const int&

In this case, any const, volatile, or reference qualifiers present on the original declaration are preserved precisely as written.

Rule 2: General Expressions and Value Categories

If the argument to decltype(e) is an expression other than an unparenthesized id-expression (including parenthesized variable names), decltype determines the type based on the value category of the expression:

  1. Lvalue: If e has type T and is an lvalue, decltype(e) yields T&.
  2. Xvalue: If e has type T and is an xvalue (an expiring value, such as a moved temporary), decltype(e) yields T&&.
  3. Prvalue: If e has type T and is a prvalue (a pure rvalue, like a literal or temporary object), decltype(e) yields T.
struct Point { int x; };
Point p;

decltype(p.x) t1;       // Id-expression: int
decltype((p.x)) t2 = p.x; // Parenthesized expression, lvalue: int&
decltype(10 + 5) t3;    // Arithmetic expression, prvalue: int
decltype(std::move(p)) t4 = std::move(p); // Xvalue: Point&&

The Parenthesis Distinction

The difference between decltype(variable) and decltype((variable)) is a direct consequence of these rules. Wrapping an identifier in parentheses turns an id-expression into an lvalue expression:

int n = 0;
decltype(n)   a = n; // int (declared type of n)
decltype((n)) b = n; // int& (parenthesized expression is an lvalue)

Practical Application with decltype(auto)

Introduced in C++14, decltype(auto) uses the exact deduction rules of decltype instead of standard auto template deduction rules (which typically strip references and top-level const qualifiers). This ensures perfect forwarding of return types in functions and templates where the value category must be preserved:

template <typename Container>
decltype(auto) get_first(Container& c) {
    return c[0]; // If operator[] returns a reference, decltype(auto) deduces T&
}