The Speaking Machine

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:

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

#auto #blog #cpp #decltype #programming #references #type-deduction