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

💡 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 30

2. 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 30

Your 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: 6

4. 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

🧠 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)

📋 Quick Reference

FormDeduces
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

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

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.

Related lessons