auto & decltype
Reviewed & published by Brayan K
Some C++ types are long and ugly — iterator types, template results, lambda types. auto lets the compiler figure them out for you, and decltype reports the exact type of an expression when you need precision. Used well, they make code shorter and safer; used carelessly, they hide important detail. This lesson teaches you when each one earns its keep.
Part of the free C++ course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Let the compiler deduce types with auto
- Why auto drops const and references — and how to keep them
- Use auto&, const auto&, and auto&& in range-based loops
- Report an expression's exact type with decltype
- Deduce function return types with auto and trailing return
- Know when explicit types read better than auto
💡 Real-World Analogy
Imagine filling out a form. With explicit types you write the full label every time: "this box holds a 32-bit signed integer". With auto you instead say "whatever fits what I'm putting in" — you hand over a value and the form figures out the right box. It's not weaker typing: the box still has exactly one fixed type, you just let the value decide it. decltype is like asking a clerk "what type is that field over there?" — it tells you the precise declared type of something that already exists, without touching it.
1. auto: Deduce the Type
auto asks the compiler to deduce a variable's type from its initializer. It's still strongly, statically typed — once deduced, the type is locked in. The classic payoff is iterators: instead of writing std::vector<int>::iterator, you write auto. Note that an auto variable must be initialized, since there's nothing to deduce from otherwise.
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int main() {
// auto deduces the type from the value on the right.
auto i = 42; // int
auto d = 3.14; // double
auto c = 'A'; // char
auto s = string("hi"); // std::string
auto b = true; // bool
cout << i << " " << d << " " << c << " " << s << " " << b << endl;
// 42 3.14 A hi 1
// The big win: long iterator types become one word.
vector<int> v = {10, 20, 30};
for (auto it = v.begin(); it != v.end(); ++it) {
cout << *it << " "; // 10 20 30
}
cout << endl;
return 0;
}
// ✅ Expected output:
// 42 3.14 A hi 1
// 10 20 302. auto Copies — Use auto& to Avoid It
Here's the subtle part: plain auto deduces by value, so it makes a copy and strips away references and top-level const. In a range-based for loop that means for (auto n : v) modifies copies, not the real elements. Use auto& to bind a reference (to modify the originals) or const auto& to read efficiently without copying.
#include <iostream>
#include <vector>
using namespace std;
int main() {
vector<int> nums = {1, 2, 3};
// Plain 'auto' makes a COPY -> changes don't stick.
for (auto n : nums) n *= 10; // modifies copies only
cout << nums[0] << endl; // 1 (unchanged!)
// 'auto&' is a REFERENCE -> changes the real elements.
for (auto& n : nums) n *= 10; // modifies originals
cout << nums[0] << endl; // 10
// 'const auto&' = read-only, and avoids copying big objects.
for (const auto& n : nums) cout << n << " "; // 10 20 30
cout << endl;
return 0;
}
// ✅ Expected output:
// 1
// 10
// 10 20 30Your turn. The loop below should double each price in place. Fill in the blank with the right form of auto:
#include <iostream>
#include <vector>
#include <string>
using namespace std;
int main() {
// 🎯 YOUR TURN — double every price IN PLACE.
vector<double> prices = {1.5, 2.0, 4.0};
// Use a reference so the change sticks. Fill in the blank:
for (___ p : prices) { // 👉 auto&
p *= 2;
}
for (double p : prices) cout << p << " ";
cout << endl;
// ✅ Expected output: 3 4 8
return 0;
}🧩 Reorder Challenge
These lines declare a value with auto, derive a decltype type from it, and print. Put them in order:
A. decltype(price) tax = 1.25;
B. auto price = 4.0;
C. cout << price + tax << endl;
D. int main() {
E. return 0;
F. }Correct order: D → B → A → C → E → F
Open main (D), let price deduce to double (B), declare tax with the same type via decltype(price) (A), print the sum 5.25 (C), then return 0; (E) and close (F). decltype(price) needs price to exist first.
3. decltype: the Exact Type of an Expression
Where auto deduces from a value and strips references and const, decltype(expr) reports the exact declared type of an expression — including const and references — and it does not evaluate the expression. This makes it the precise tool when you must mirror the type of something that already exists, such as the result type of a + b in a template.
#include <iostream>
#include <typeinfo>
using namespace std;
int main() {
int x = 5;
const int cx = 10;
int& rx = x;
// decltype(expr) is the EXACT declared type of expr (not evaluated).
decltype(x) a = 1; // int
decltype(cx) b = 2; // const int (keeps const!)
decltype(rx) c = x; // int& (keeps the reference!)
a += 100;
c += 1; // c refers to x -> x changes too
cout << "a = " << a << endl; // a = 101
cout << "x = " << x << endl; // x = 6 (changed via c)
// b = 99; // ❌ error: b is const int
// decltype shines for the type of an EXPRESSION:
decltype(x + 1.0) sum = x; // x + 1.0 is double -> sum is double
cout << "sum type holds: " << sum << endl; // 6
return 0;
}
// ✅ Expected output:
// a = 101
// x = 6
// sum type holds: 64. Deduced Return Types
Since C++14, a function can use auto as its return type and the compiler deduces it from the return statements — invaluable for templates where the result type depends on the inputs. The older trailing return type syntax, auto f(...) -> decltype(...), spells the type out explicitly using decltype, which is occasionally clearer or required.
#include <iostream>
using namespace std;
// A template where the return type depends on the arguments.
// 'auto' return type (C++14) lets the compiler deduce it from 'return'.
template <typename A, typename B>
auto multiply(A a, B b) {
return a * b; // type deduced: int*double -> double, etc.
}
// Trailing return type with decltype spells it out explicitly:
template <typename A, typename B>
auto add(A a, B b) -> decltype(a + b) {
return a + b;
}
int main() {
cout << multiply(3, 2.5) << endl; // 7.5 (int * double -> double)
cout << multiply(4, 5) << endl; // 20 (int * int -> int)
cout << add(1, 2) << endl; // 3
cout << add(1, 2.5) << endl; // 3.5
return 0;
}
// ✅ Expected output:
// 7.5
// 20
// 3
// 3.5🔎 Deep Dive: auto's deduction rules & decltype(auto)
Plain auto follows the same rules as template argument deduction: it deduces by value, dropping top-level const and references. You opt back in explicitly:
const int ci = 10;
auto a = ci; // int (const dropped)
auto& b = ci; // const int& (you asked for a reference)
const auto& c = ci; // const int& (read-only reference, no copy)
auto&& d = 5; // forwarding reference (advanced)decltype(auto) is a special return-type form that uses decltype's rules instead of auto's, so it preserves references and const. It's mainly for generic wrappers that must forward an expression's exact type:
decltype(auto) firstElement(vector<int>& v) {
return v[0]; // returns int& -- the reference is preserved
}Pro Tips
- 💡 Use const auto& in loops over big objects: it reads them without copying.
- 💡 Reach for auto on iterators & lambdas: these types are long or unnameable.
- 💡 Spell the type out when it aids the reader: auto that hides a surprising type hurts clarity.
- 💡 Remember auto needs an initializer: auto x; won't compile — there's nothing to deduce from.
🧠 Quick Recall
Predict the output before revealing the answer.
vector<int> v = {1, 2, 3};
for (auto n : v) n += 100;
cout << v[0];1 — plain auto copies each element, so n += 100 changes copies, not the vector. Use auto& to actually modify it.
2. What type is x, and what prints?
auto x = 7 / 2;
cout << x;3 — 7 / 2 is integer division giving an int 3, so x is deduced as int. auto doesn't change the math.
3. Does b change a?
int a = 5;
decltype(a)& b = a;
b = 99;
cout << a;99 — decltype(a) is int, so b is an int& bound to a. Assigning to b updates a.
Common Errors (and the fix)
- Uninitialized auto: auto x; fails — there's no value to deduce from. Give it an initializer.
- Expecting a loop to mutate with plain auto: for (auto e : v) copies. Use auto& to change elements.
- Unintended copies of big objects: for (auto s : strings) copies every string. Prefer const auto&.
- Confusing decltype(x) with decltype((x)): the extra parentheses make it a reference type (int& instead of int). The parens matter.
- Over-using auto: when the deduced type surprises a reader, write it out — clarity beats brevity.
📋 Quick Reference
| Form | Deduces |
|---|---|
| auto x = v; | by value, drops const & ref |
| auto& x = v; | reference to v |
| const auto& x = v; | read-only ref, no copy |
| decltype(expr) | exact type, keeps const & ref |
| auto f() { ... } | return type from return stmt |
| auto f() -> decltype(e) | trailing return type |
Mini-Challenge: Iterate a Map with auto
Put const auto& and structured bindings to work iterating a map, and let auto hold your running total.
#include <iostream>
#include <map>
#include <string>
using namespace std;
int main() {
// 🎯 MINI-CHALLENGE: Iterate a map cleanly with auto
// 1. Given the scores map below, loop over it using
// 'const auto&' and structured bindings: for (const auto& [name, score] : scores)
// 2. Print "name: score" for each entry.
// 3. BONUS: keep a running 'auto total = 0;' and print the sum.
//
map<string, int> scores = {{"Ana", 90}, {"Ben", 75}, {"Cy", 88}};
//
// ✅ Example output (sorted by key):
// Ana: 90
// Ben: 75
// Cy: 88
// total: 253
// your code here
return 0;
}🎉 Lesson Complete
- ✅ auto deduces a variable's type from its initializer (and must be initialized)
- ✅ Plain auto copies — use auto& to modify, const auto& to read big objects
- ✅ decltype(expr) gives the exact type, keeping const and references
- ✅ Functions can deduce return types via auto or trailing -> decltype(...)
- ✅ decltype(auto) forwards an expression's exact type in generic code
Practice quiz
What does 'auto' do in 'auto x = 5;'?
- Makes x dynamically typed
- Makes x a reference
- Deduces x's type (int) from the initializer at compile time
- Defers x's type until runtime
Answer: Deduces x's type (int) from the initializer at compile time. auto deduces the type from the initializer; the type is still fixed and checked at compile time.
In a range loop, 'for (auto n : v) n *= 10;' fails to change the vector because:
- Plain auto deduces by value, so n is a copy
- auto is read-only
- The loop never runs
- 10 is too large
Answer: Plain auto deduces by value, so n is a copy. Plain auto copies each element; use 'auto&' to bind a reference and modify the originals.
Which form modifies the container's elements AND avoids copying when only reading is needed?
- auto& for both
- const auto for both
- decltype(auto) only
- auto for writing; const auto& for reading
Answer: auto for writing; const auto& for reading. Use auto& to modify originals, and const auto& to read big objects without copying.
What does plain 'auto' drop during deduction?
- Nothing
- Top-level const and references
- Only references
- Only const
Answer: Top-level const and references. Plain auto deduces by value, stripping top-level const and references; opt back in with auto&, const auto&, etc.
How does decltype(expr) differ from auto?
- It reports the exact declared type, keeping const and references
- It evaluates the expression
- It only works on integers
- It is the same as auto
Answer: It reports the exact declared type, keeping const and references. decltype gives the precise declared type (including const and &) without evaluating the expression.
What is the type of x in 'auto x = 7 / 2;' and what does it print?
- double, 3.5
- int, 4
- int, 3
- float, 3.5
Answer: int, 3. 7 / 2 is integer division giving int 3; auto deduces int and doesn't change the math.
Given 'int a = 5; decltype((a)) c = a;', what is c's type?
- int (a copy)
- int& (a reference to a)
- const int
- double
Answer: int& (a reference to a). The extra parentheses make decltype((a)) yield int& — a reference — so assigning to c changes a.
What does the trailing-return syntax 'auto add(A a, B b) -> decltype(a + b)' do?
- Always returns int
- Returns void
- Forbids deduction
- Spells out the return type explicitly using decltype of the expression
Answer: Spells out the return type explicitly using decltype of the expression. It declares the return type after the parameters using decltype, useful when the result type depends on the arguments.
Why does 'auto x;' fail to compile?
- auto is reserved
- There is no initializer for the compiler to deduce the type from
- x must be uppercase
- auto needs a semicolon
Answer: There is no initializer for the compiler to deduce the type from. An auto variable must be initialized, since the type is deduced from the initializer.
What is decltype(auto) mainly used for?
- Looping faster
- Declaring globals
- Forwarding an expression's exact type (preserving references/const) in generic code
- Disabling const
Answer: Forwarding an expression's exact type (preserving references/const) in generic code. decltype(auto) uses decltype's rules so a generic return can preserve the exact type and value category of an expression.
Continue this course
- Previous: Maps & Sets
- Next: Range-Based for Loops — Iterate containers cleanly with for (auto& x : c)
- Quick reference: C++ cheat sheet › Modern C++
Frequently asked questions
What does auto actually do?
auto tells the compiler to deduce a variable's type from the value you initialize it with. The type is still fixed and checked at compile time; auto just saves you from spelling it out. For example, auto x = 5; makes x an int.
When should I use auto and when should I avoid it?
Use auto when the type is obvious from the right-hand side, very long to write (like iterator types), or impossible to name (like a lambda). Avoid it when an explicit type makes the code clearer for a reader, or when you want to force a specific type.
What is the difference between auto and decltype?
auto deduces a type from an initializer and drops top-level const and references by default. decltype(expr) reports the exact declared type of an expression, including const and references, without evaluating it. Use auto for variables and decltype when you need the precise type of an existing expression.
Why does auto drop references and const?
Plain auto deduces by value, copying the result, so references and top-level const are stripped. If you want to keep them, write auto& for a reference, const auto& for a read-only reference, or auto&& for a forwarding reference.
What is decltype(auto)?
decltype(auto) deduces the type using decltype's rules instead of auto's, which preserves references and const. It is mainly used for return types in generic code where you must forward the exact type of an expression, including its value category.