Skip to content
VLSI Mentor

C++ Part I · Module 21 · Modern C++ Type and Utility Tools

decltype and Deduced Types in Interfaces

Chapter 21.1 named a type you already knew. This chapter is about the case where you genuinely cannot: a generic function whose return type depends on what its arguments turn out to be, a wrapper that must return exactly what the thing it wraps returned, a trait that needs to ask what an expression would produce without producing it. decltype answers all three by yielding the declared type of an expression, and it is the one tool in this module that is almost entirely a compile-time query rather than a thing that exists at run time. Two of its properties are worth the chapter's length. The first is that it preserves const and references exactly, where auto strips both — a difference the chapter measures on one identical initialiser, giving int from auto and const int& from decltype, which is why the two are not interchangeable and why decltype(auto) exists as a third option. The second is a trap small enough to be a typographical accident and large enough to produce undefined behaviour: wrapping the operand in one extra pair of parentheses changes the answer from the type of the entity to the type of an expression, and for a named variable that means int becomes int reference. In a decltype(auto) return statement that difference turns a returned value into a returned reference to a local that has just been destroyed, and the chapter measures which tools notice.

Advanced18 min readC++Part IFundamentalsModern C++

Part I · Module 21 · Chapter 21.3 · decltype and Deduced Types in Interfaces

1. What It Yields

decltype(e) is the declared type of the expression e. It is evaluated by the compiler, produces a type rather than a value, and e is never executed — §6 measures that last point, which is the property that makes traits possible.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int x = 1; const int cx = 2; int& rx = x;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
decltype(x)   = int
decltype(cx)  = const int
decltype(rx)  = int&   <- the reference is KEPT
decltype(x+1) = int   (an expression, not a variable)

Three things to note. const is preserved. The reference is preserved — decltype(rx) is int&, not int. And the operand need not be a variable: decltype(x+1) asks what the expression x+1 would produce, which is int.

That faithfulness is the whole point, and it is what separates decltype from auto.

2. The Parentheses Rule

This is the trap, and it is worth memorising because it is invisible in review.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int x = 1;
decltype(x)    // ?
decltype((x))  // ?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
decltype(x)   = int    (the ENTITY x: its declared type)
decltype((x)) = int&   (an EXPRESSION: an lvalue, so T&)
is_same<decltype(x), decltype((x))> = 0

The rule is: if the operand is an unparenthesised name of a variable, decltype gives that variable's declared type. For any other expression, it gives the expression's type with a reference added according to its value category — T& for an lvalue, T&& for an xvalue, T for a prvalue.

A parenthesised name is "any other expression". (x) is an lvalue expression of type int, so the answer is int&.

This is not a quirk invented to trip people up; it is what makes decltype usable for the value-category-sensitive work in §5. But it means one pair of parentheses changes the answer, and §8 is the consequence.

3. decltype Versus auto on the Same Initialiser

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
const std::vector<int> v{1,2,3};
auto           a = v[0];
decltype(v[0]) d = v[0];
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
auto           a = v[0] -> is_same<int>        = 1
decltype(v[0]) d = v[0] -> is_same<const int&> = 1

Identical initialiser, different types. v[0] on a const std::vector<int> returns const int&. auto strips the reference and the top-level const, leaving int — a copy. decltype reports exactly what the expression produces.

That is the same stripping rule Chapter 18.3 §3 measured for template argument deduction, because auto deduction and template deduction use the same rules. decltype deliberately does not.

Neither behaviour is better. auto a = v[0] giving a copy is almost always what you want for a local. decltype(v[0]) is what you want when you are declaring something that must match the expression's type exactly — a return type, a trait, a member of a wrapper.

The practical shorthands worth knowing: auto for a copy, const auto& for a non-copying read, auto& for a mutable reference, and decltype(auto) when the thing must be whatever the initialiser was.

4. Where It Is Genuinely Necessary

A generic function whose return type depends on its arguments cannot name that type.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
template <typename A, typename B>
auto addTrailing(A a, B b) -> decltype(a + b) { return a + b; }

template <typename A, typename B>
auto addDeduced(A a, B b) { return a + b; }            // C++14
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
addTrailing(1, 2.5)  = 3.5  (decltype(a+b) -> double)
addDeduced(1, 2.5)   = 3.5  (C++14 `auto` does the same)
is_same<decltype(addTrailing(1,2.5)), double> = 1

The trailing return type exists for this. A return type written before the parameter list cannot mention a and b, because they are not in scope yet; auto f(...) -> decltype(...) moves it after the parameters, where they are.

Since C++14 a plain auto return type deduces from the return statement, which is shorter and usually better. So the trailing form is now needed only when the return type must be stated rather than deduced — which happens when you want it visible in the declaration, when the function is declared separately from its definition, or when SFINAE depends on it.

That last case is worth noting because it is how pre-concepts constraint code was written: a trailing decltype that fails to form a type removes the overload from consideration. Chapter 21.8's concepts replace it, and the comparison there is dramatic.

5. decltype(auto): Preserving the Reference

auto as a return type strips references. When a wrapper must return exactly what it wrapped, that is wrong:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
template <typename C> auto           firstByValue(C& c) { return c[0]; }   // a COPY
template <typename C> decltype(auto) firstByRef(C& c)   { return c[0]; }   // the reference
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after firstByRef(v) = 99 -> v[0] = 99
firstByValue(v) returned a copy; setting it left v[0] = 99
is_same<decltype(firstByRef(v)), int&>  = 1
is_same<decltype(firstByValue(v)), int> = 1

firstByRef(v) = 99 is an assignment to the container's element, and it worked — the returned int& refers to v[0]. The auto version returned a copy, and assigning to that copy changed nothing.

decltype(auto) means "deduce the type using decltype's rules rather than auto's". It is the right return type for a forwarding wrapper, an accessor that should behave like the member it exposes, and anything in the shape of std::get or operator[].

It is also where §2's rule becomes dangerous, which is §8.

6. decltype Does Not Evaluate Its Operand

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
int calls = 0;
auto f = [&calls]() -> int { ++calls; return 1; };
using R = decltype(f());                  // is f called?
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after `using R = decltype(f())`, f was called 0 times
is_same<R,int> = 1  -- the type is known without running anything
an actual call makes it 1

Zero calls. decltype(f()) asks what f() would return, and the compiler answers from the signature without generating a call. The counter proves it.

This is what makes decltype the basis of type queries. An expression inside decltype is in an unevaluated context, along with sizeof, noexcept(...) and typeid — the compiler type-checks it and then discards it.

The practical consequence: an expression with side effects inside decltype has no side effects, which is correct and occasionally surprising when someone writes decltype(++i) expecting i to change.

7. declval: a Value You Cannot Construct

Unevaluated contexts allow a companion trick. To ask "what would T::member + 1 produce?" you need a T — but you may not be able to make one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
struct NoDefault { int v; explicit NoDefault(int x) : v(x) {} };
using Sum = decltype(std::declval<NoDefault>().v + 1);
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
decltype(declval<NoDefault>().v + 1) is int? 1

std::declval<T>() is declared to return T&& and is never defined, so calling it in evaluated code fails to link. Inside decltype that does not matter, because nothing is called. It is the standard way to name a value of a type for the purpose of asking a question about it, and it is how most of <type_traits> is written.

The rule is absolute: declval only ever appears inside an unevaluated context.

8. The Parentheses Consequence

Now the two halves meet. decltype(auto) deduces using decltype's rules, and §2's rule says a parenthesised name is an lvalue expression:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static decltype(auto) byValue() { int local = 1234; return local;   }   // int
static decltype(auto) byRef()   { int local = 1234; return (local); }   // int&
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
is_same<decltype(byValue()), int>  = 1
is_same<decltype(byRef()),   int&> = 1   <- one pair of parens

byRef returns a reference to a local variable that is destroyed when the function returns. One pair of parentheses, and the function went from correct to undefined behaviour.

Three measurements on what happens next, and the pattern is unusual.

The compiler warns, reliably:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dtparen.cpp:7:61: warning: reference to stack memory associated with local
    variable 'local' returned [-Wreturn-stack-address]

and it fires at -O0, -O1 and -O2 — one warning at each of the three levels.

At run time the simple case looks fine. Reading through the returned reference printed 1234 — the right value — at every optimisation level, because nothing had reused the frame yet. With a real function call in between to reuse it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
through the dangling reference: 1   (clobber returned 0xDEAD)

1 instead of 1234.

And AddressSanitizer was silent in every variant tested.

That combination is the exact inverse of Chapter 20.8 §7's dangling lambda capture, which produced zero compile warnings and was caught by ASan as stack-use-after-scope. Same category of bug — a reference outliving its referent — and opposite detection. The practical lesson is the one that module kept arriving at: which tool catches a bug is a property of the bug's shape, not its severity, so "the sanitizers are clean" and "the build is warning-free" are different claims and neither implies the other.

Here the compiler is the one that works, which makes the habit simple: never ignore -Wreturn-stack-address, and treat return (expr); in a decltype(auto) function as a bug until proven otherwise.

9. When Not to Use It

decltype in a signature is a cost to every future reader, so the bar should be high.

If you can name the type, name it. An alias from Chapter 21.1 is clearer than a decltype expression.

If auto return-type deduction suffices, use it. Since C++14 that covers most of what the trailing form was for.

If the answer is always the same type, say that type. -> decltype(a + b) on a function only ever called with two ints is obfuscation.

Prefer std::invoke_result_t or a concept to a hand-rolled decltype SFINAE expression. Chapter 21.8 §3 measures how much better the diagnostic is.

The honest summary: decltype is a tool for library and wrapper code, and appears rarely in application code that is doing its job. decltype(auto) is rarer still — a handful of places per codebase, all of them wrappers.

10. The Rules in One Place

operanddecltype giveswhy
x — an unparenthesised variable nameits declared type (measured int)the entity rule
(x) — parenthesisedint& (measured)an lvalue expression adds &
cx — a const variableconst intconst is preserved
rx — a reference variableint& (measured)the reference is preserved
x + 1 — a prvalue expressionintprvalue adds nothing
v[0] on const vector<int>const int& (measured)exactly what the call returns
f() where f returns intint, and f is not called (measured 0 calls)unevaluated context
std::declval<T>()T&&declared, never defined
declarationdeduces likereference preserved
auto a = e;template deductionno (measured: int)
const auto& a = e;template deductionyes, as const
decltype(e) a = e;decltypeyes (measured: const int&)
decltype(auto) a = e;decltypeyes
auto f() { return e; }template deductionno
decltype(auto) f() { return e; }decltypeyes — and §8's hazard

11. What to Actually Do

  • Use auto for locals and const auto& when you want a non-copying read.
  • Use decltype(auto) only for wrappers that must be transparent — accessors, forwarding returns.
  • In a decltype(auto) return, never parenthesise the expression unless you intend a reference.
  • Never suppress -Wreturn-stack-address. Measured: it is the tool that catches §8.
  • Prefer a plain auto return type to the trailing decltype form since C++14.
  • Use the trailing form when the return type must be visible in a declaration, or when SFINAE depends on it — and prefer a concept where available.
  • Remember decltype does not evaluate. Side effects inside it do not happen.
  • Use declval only inside an unevaluated context. It has no definition.
  • If you can name the type, name it — an alias beats a decltype expression for a reader.

12. Mistakes Worth Making Once

return (local); in a decltype(auto) function. Measured: int& instead of int, a compiler warning at every level, ASan silent, and the value right until the frame was reused.

Expecting auto to preserve a reference. Measured: auto a = v[0] on a const vector gives int, and writing to it changes nothing.

Expecting decltype(++i) to increment i. Unevaluated; measured zero calls for a counting lambda.

Calling declval outside decltype. It links nowhere, because it has no definition.

Using the trailing return type out of habit in C++14 or later, where plain auto would do.

decltype on an overloaded function name without disambiguating. The name alone is not an expression with one type.

Assuming a clean ASan run covers this. Measured silent. The compiler warning is the coverage here.

13. Interview Relevance

The frozen curriculum marks this chapter Medium, and the parentheses question is the one that distinguishes a read answer from a used one.

"What is the difference between auto and decltype?" auto deduces like a template parameter, stripping references and top-level const; decltype reports the expression's type exactly. The measured int versus const int& on one initialiser is the clean way to say it.

"What does decltype((x)) give, and why?" int&. An unparenthesised name means the entity; any other expression gets a reference added by value category. The follow-up is always the decltype(auto) consequence.

"When would you use a trailing return type?" When the return type depends on the parameters and must be stated rather than deduced — or before C++14, always.

"What is decltype(auto) for?" A wrapper that must return exactly what it wrapped, including the reference. std::get-shaped code.

"Does decltype evaluate its operand?" No. That is what makes <type_traits> possible, and std::declval relies on it.

"Why is std::declval never defined?" Because it is only ever used where nothing is called, and leaving it undefined guarantees a link error if you misuse it.

14. What You Should Be Able to Say Now

  • That decltype gives the declared type of an expression, preserving const and references.
  • That decltype(x) is int and decltype((x)) is int&, and the entity-versus-expression rule behind it.
  • That auto and decltype gave int and const int& from the same initialiser.
  • That decltype(auto) is for transparent wrappers, and that firstByRef(v) = 99 wrote through while the auto version did not.
  • That decltype does not evaluate its operand — measured zero calls.
  • That declval is declared and never defined, for use only in unevaluated contexts.
  • That return (local); in a decltype(auto) function returns a dangling reference, that -Wreturn-stack-address catches it at every optimisation level, and that ASan did not — the inverse of Chapter 20.8 §7.

15. Exercises

Exercise 1 — the table. Reproduce §10's first table on your compiler, adding decltype(std::move(x)), decltype(v) for a vector, and decltype(*v.begin()).

Exercise 2 — the parentheses. Write a decltype(auto) function both ways, confirm the two return types with static_assert, and read the warning.

Exercise 3 — detection. Build §8's program at -O0, -O2, with ASan and with UBSan. Tabulate warnings, exit codes and printed values. Then do the same for Chapter 20.8's dangling capture and compare the two rows.

Exercise 4 — transparency. Write an at() wrapper over std::vector three ways — auto, auto&, decltype(auto) — and show which ones permit w.at(0) = 5.

Exercise 5 — unevaluated. Put a counter-incrementing call inside decltype, sizeof and noexcept and show the counter stays at zero for all three.

Exercise 6 — declval. Write a trait that reports whether T has a member size() returning something convertible to size_t, using declval and a trailing decltype. Then write it again with a C++20 concept and compare the failure messages.

Exercise 7 — the honest case. Find one place in your own code where decltype is genuinely necessary and one where it is decoration. Replace the second with an alias.

16. Summary

  • decltype(e) is the declared type of e, preserving const and references exactly.
  • Measured: decltype(x) is int, decltype((x)) is int& — an unparenthesised name means the entity, anything else means the expression and adds a reference by value category.
  • Measured: from one initialiser, auto gave int and decltype(v[0]) gave const int& — auto deduces like a template parameter and strips both.
  • A trailing return type exists so the return type can mention the parameters; since C++14 plain auto covers most of it.
  • Measured: decltype(auto) preserves the reference — firstByRef(v) = 99 wrote through to v[0] where the auto version returned a copy.
  • Measured: decltype does not evaluate its operand — zero calls to a counting lambda — which is what makes <type_traits> and std::declval work.
  • Measured: return (local); in a decltype(auto) function returns int& to a destroyed local. -Wreturn-stack-address fires at -O0, -O1 and -O2; the value read as the correct 1234 until a real call reused the frame, then 1; and ASan was silent — the inverse of Chapter 20.8 §7.
  • So a clean sanitiser run and a clean build are independent evidence, and this bug is caught only by the second.
  • Use auto for locals, decltype(auto) for transparent wrappers, and an alias when you can name the type.

Three chapters have each dealt with a single type — naming one, computing with one, querying one. Chapter 21.4 moves to carrying several values at once, which is the first place in this module where the right answer is often not the library tool. std::pair and std::tuple are genuinely useful for a short-lived grouping, and the chapter measures that they cost exactly what the equivalent struct costs in bytes — and then measures the one property where they differ, which turns out not to be size at all.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.