decltype, Demystified
The auto type specifier deduces a type from an initializer — but sometimes you want the type of an expression without actually evaluating it, or you want to keep details that auto deliberately throws away (like top-level const and references). That's the job of decltype. It asks the compiler a simple question — "what is the type of this expression?" — and gives you that type to declare a variable with.
In this post, I will walk through what decltype does, how it differs from auto, and the handful of corner cases that surprise nearly everyone.
The basic idea: a type without an evaluation
decltype(e) yields the type of the expression e. Crucially, e is not evaluated — the compiler only inspects its type:
int f() { return 1024; }
decltype(f()) sum = 23; // sum has whatever type f returns (int); f is NOT called
Here f() is never invoked. The compiler looks at f's declared return type, decides sum is an int, and moves on. This is the headline difference from auto: auto needs a real initializer whose value it deduces from, while decltype reasons purely about types.
decltype preserves what auto discards
When auto deduces from an expression, it strips top-level const and references — it gives you the "plain value" type. decltype does the opposite: it reports the type exactly as declared, const and references included.
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x has type const int (top-level const preserved)
decltype(cj) y = x; // y has type const int& (reference preserved), bound to x
Compare this with auto b = ci;, which would deduce a plain int (dropping the const). decltype(ci) keeps it. This makes decltype the right tool when you need a type to match an existing declaration faithfully rather than a simplified copy of it.
A decltype reference must still be initialized
Because decltype(cj) is a reference type, the usual rule applies — a reference has to be bound when it's created:
const int ci = 0, &cj = ci;
decltype(cj) z; // error: z is a reference (const int&) and must be initialized
The error isn't about decltype; it's the ordinary "references must be
initialized" rule showing up because decltype faithfully produced a reference type.
The subtle one: variables vs. expressions
This is the rule that trips people up, so it deserves its own section.
decltype treats a bare variable name differently from a more general expression:
decltype(variable)gives the variable's declared type, exactly as written.decltype(expression), where the expression is something that could appear on the left-hand side of an assignment (an lvalue), gives a reference to that type.
And here's the kicker: wrapping a variable in parentheses turns it into an expression. decltype((var)) is always a reference type, because a parenthesized variable is an lvalue expression, not a bare declaration.
int i = 42;
decltype(i) a; // int — the declared type of i (a is uninitialized but legal)
decltype((i)) b; // int& — ERROR: int& must be initialized!
That extra pair of parentheses changes int into int&. The mnemonic worth memorizing:
decltype((variable))(double parentheses) is always a reference type.decltype(variable)is a reference type only if the variable was itself declared as a reference.
Assignment expressions are lvalues too, which is a more realistic way to hit this:
int i = 0;
decltype(i = i + 1) ref = i; // ref is int& — the assignment is NOT performed,
// decltype only inspects the expression's type
As with f() earlier, the assignment inside decltype(...) never actually runs.
decltype vs. auto at a glance
auto |
decltype |
|
|---|---|---|
| Deduces from | the initializer's value | an expression's type |
| Evaluates the expression? | n/a (uses the initializer) | no |
Top-level const |
dropped | kept |
| References | dropped (deduces the referent's type) | kept |
| Parentheses around a name | no effect | ((x)) forces a reference |
A short way to remember it: reach for auto when you want "the value's type, kept simple," and reach for decltype when you want "this exact type, warts and all."
A common use: matching a return type to an expression
decltype shines when you want a variable — or a function's return type — to track the type of some expression without spelling it out, and without the simplification auto would impose. A typical pattern is computing a result type from operands:
// the type of multiplying a T by a U, whatever that turns out to be
decltype(a * b) product = a * b;
This keeps product in lockstep with whatever a * b actually produces, even if you later change the types of a and b. (In modern generic code this idea generalizes to trailing return types and decltype(auto), but the core intuition is the same one shown above.)
Some key takeaways
decltype(e)yields the type ofewithout evaluatinge— function calls and assignments inside it never run.- Unlike
auto,decltypepreserves top-levelconstand reference-ness, reporting the type exactly as declared. decltype(variable)gives the variable's declared type;decltype(expression)on an lvalue gives a reference type.decltype((variable))— with double parentheses — is always a reference, so it must be initialized.- Use
autofor "the value's type, simplified"; usedecltypefor "this exact type, preserved."