---
title: 'Type Deduction'
source: 'https://academia.sh/en/courses/cpp-standard-library/type-deduction'
course: 'Resource Management and the Standard Library'
language: en
updated: '2026-09-26T03:02:47+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - "Explains how auto drops an expression's const and reference."
    - 'Distinguishes auto&, const auto&, and auto&& by their effect on the source.'
    - 'Shows that decltype and decltype(auto) deduce types by different rules.'
    - 'Confirms that copying versus binding by reference in a structured binding leaves a different result in the source.'
---

# Type Deduction

How auto drops const and reference, auto&/const auto&/auto&&, decltype and decltype(auto), the copy-versus-reference difference in structured bindings, and what type deduction leaves in the source.

The previous lesson showed that a view leaves the container **unchanged**
when constructed from an lvalue, and turns the container **empty**
through the move constructor when constructed from an rvalue. In both
examples, `auto` was standing somewhere — instead of writing the view's
actual type, we left it to the compiler. This lesson opens up what that
shortcut does: how `auto` deduces an expression's type, in which case it
drops `const` and the reference, and with which spelling it does not
drop them. This course's central question is the same here too: what
does a type deduction leave in the source?

## What auto Keeps, What It Drops

`auto` deduces a type from an initializer expression; but the type it
deduces is not identical to the expression's type. The rule is this:
when `auto` is written by itself, it **drops the expression's const and
reference**, leaving only the underlying type behind. The example below
shows this.

```cpp
// auto-drop.cpp — how auto drops const and reference
#include <cstdio>

int main() {
    const int constant = 42;
    int value = 10;
    int& reference = value;

    auto a = constant;    // int -- const dropped
    auto b = reference;   // int -- reference dropped

    a = 99;
    b = 99;

    std::printf("constant %d, value %d\n", constant, value);
    std::printf("a %d, b %d\n", a, b);
    return 0;
}
```

```text
constant 42, value 10
a 99, b 99
```

`a` and `b` are each a copy of `constant` and `reference`; writing to
either does not affect the source. `constant` stays at 42, `value` stays
at 10. **SL39.** `auto` produces an independent copy by dropping an
expression's const and reference; writing to the copy leaves nothing in
the source — the source stays **unchanged**, because the copy itself is
a separate object.

## Binding to a Reference with auto&

When `auto&` is written, the reference does not drop; the deduced type
is a reference bound to the initializer itself. This is behavior
opposite to `auto`: not a copy, but the source itself.

```cpp
// auto-reference.cpp — auto& binds to the source itself, not a copy
#include <cstdio>

int main() {
    int value = 10;
    int& reference = value;

    auto& binding = reference;   // int& -- reference does not drop
    binding = 99;

    std::printf("value %d\n", value);
    return 0;
}
```

```text
value 99
```

The assignment made through `binding` reaches `value`; `auto&` did not
forget that `reference` was a reference. The only difference between
`auto` and `auto&` is a single `&` mark, but the result is reversed: one
builds a copy, the other binds to the source itself.

## const auto&: Read-Only Without a Copy

While traversing a container just to read it, `auto` copies every
element. If the element is large (a `std::string`, a record), this
copying is an unnecessary cost. `const auto&` binds the source itself in
a read-only form, without taking a copy.

```cpp
// const-auto-reference.cpp — reading a large element without copying it
#include <cstdio>
#include <string>
#include <vector>

int main() {
    std::vector<std::string> stock{"apple", "pear", "cherry"};

    for (const auto& name : stock) {
        std::printf("%s\n", name.c_str());
    }

    std::printf("element count %zu\n", stock.size());
    return 0;
}
```

```text
apple
pear
cherry
element count 3
```

No `std::string` was copied throughout the loop; `name` was bound to
`stock`'s current element at each step. `const` tells the compiler that
no writing will happen through this binding, and `stock` stays
**unchanged** at the end of the loop. Not taking a copy here is a
performance decision, it does not change the effect on the source: both
copying `auto` and `const auto&` leave the source untouched during a
read — the difference is only whether an intermediate copy exists.

## auto&&: Forwarding Reference

When `auto&&` is written, the deduced type changes according to the
initializer's value category: it becomes an ordinary reference (`T&`)
when initialized with an lvalue, and an rvalue reference (`T&&`) when
initialized with an rvalue. The `auto&&` form that shows this dual
behavior is called a **forwarding reference**.

```cpp
// auto-forwarding-reference.cpp — auto&& deduces a different type by value category
#include <cstdio>
#include <string>

int main() {
    std::string name = "apple";

    auto&& lvalue_binding = name;                     // string&
    auto&& rvalue_binding = std::string("pear");       // string&&

    lvalue_binding += " (updated)";

    std::printf("name: %s\n", name.c_str());
    std::printf("rvalue: %s\n", rvalue_binding.c_str());
    return 0;
}
```

```text
name: apple (updated)
rvalue: pear
```

Because `lvalue_binding` is bound to `name` itself, the append made
through it reaches `name`. `rvalue_binding`, in turn, was bound to a
temporary object and extended its lifetime to the end of the statement.
A forwarding reference only appears through deduction in the form
`auto&&` or a template parameter `T&&`; a hand-written `std::string&&`
does not show this behavior, because its type is already fixed, it is
not deduced.

## decltype and decltype(auto)

`decltype` follows a different rule from `auto`: it gives an
expression's type **exactly as it is**, without changing it. Where `auto`
drops the reference, `decltype` does not.

```cpp
// decltype-example.cpp — decltype does not drop the reference
#include <cstdio>

int main() {
    int value = 10;
    int& reference = value;

    decltype(value) a = 1;        // int
    decltype(reference) b = value; // int& -- reference does not drop

    b = 42;

    std::printf("value %d, a %d\n", value, a);
    return 0;
}
```

```text
value 42, a 1
```

`b` took the type `int&` thanks to `decltype(reference)` and was bound
to `value` itself; `a`, on the other hand, is an ordinary `int` copy.
`auto` would have reduced both to `int` here.

`decltype(auto)` is used to deduce a function's return type by
`decltype`'s rules — but it carries a trap: **wrapping the return
expression in parentheses** changes the deduced type. The two functions
below appear to return the same variable, but one deduces a value, the
other a reference.

```cpp
// decltype-auto-parentheses.cpp — parentheses turn the expression into an lvalue and change the type
#include <cstdio>
#include <type_traits>

int counter = 0;

decltype(auto) increment_and_return() {
    ++counter;
    return counter;     // decltype(auto) -> int (a variable name by itself)
}

decltype(auto) increment_and_return_parenthesized() {
    ++counter;
    return (counter);   // decltype(auto) -> int& (a parenthesized expression is an lvalue)
}

int main() {
    constexpr bool first_is_value = std::is_same_v<decltype(increment_and_return()), int>;
    constexpr bool second_is_reference =
        std::is_same_v<decltype(increment_and_return_parenthesized()), int&>;

    std::printf("is the first a value: %s\n", first_is_value ? "yes" : "no");
    std::printf("is the second a reference: %s\n", second_is_reference ? "yes" : "no");
    return 0;
}
```

```text
is the first a value: yes
is the second a reference: yes
```

`decltype(auto)` sees `return counter;` as a plain variable name by
itself and deduces `int`. For `return (counter);` it reads the
parenthesized expression as an lvalue and deduces `int&`. Both functions
work safely here because `counter` itself is defined at namespace scope;
but returning a **local** variable defined inside a function the same
way with parentheses produces a reference to an object that no longer
exists once the function ends — undefined behavior is entered even
before the caller reads that reference. This is why the parentheses in a
`decltype(auto)` return are not a spelling choice that can be ignored.

## Structured Bindings: Copy or Reference

A structured binding splits a record type (a `std::pair`, an element of a
`std::map`) into named parts in a single line: `auto [name, count] :
stock`. The `auto` here follows the same rule — it copies the source; if
you want a reference, you need to add `&`. Because an unordered
container's traversal order is undefined, the example below uses
`std::map`; since it is ordered by key, its output is stable.

```cpp
// structured-binding.cpp — the difference between copying and referencing structured binding
#include <cstdio>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> stock{{"apple", 4}, {"pear", 2}};

    for (auto [name, count] : stock) {
        count += 100;   // changes only the copy
    }

    std::printf("after the copy:");
    for (const auto& [name, count] : stock) {
        std::printf(" %s=%d", name.c_str(), count);
    }
    std::printf("\n");

    for (auto& [name, count] : stock) {
        count += 100;   // changes the source itself
    }

    std::printf("after the reference:");
    for (const auto& [name, count] : stock) {
        std::printf(" %s=%d", name.c_str(), count);
    }
    std::printf("\n");
    return 0;
}
```

```text
after the copy: apple=4 pear=2
after the reference: apple=104 pear=102
```

The first loop bound a copy of each element with `auto [name, count]`;
`count += 100` wrote only to that copy, and `stock` appears unchanged on
the "after the copy" line. The second loop bound the elements themselves
with `auto& [name, count]`; the same `+= 100` this time reached `stock`
itself. **SL40.** In structured bindings too, the kind of the type
deduction depends on the `&` before the square brackets: `auto [...]`
leaves the source **unchanged**, `auto& [...]` **changes** the source —
both split the same record type with the same names, the difference is
only in that single character.

## The Readability Trade-Off

`auto` is a shortcut, but it does not increase readability everywhere.
When the type name tells the reader what the source is and which
operations are valid, it should be written out: writing `auto it` instead
of an iterator's long template name puts the reader at ease, because
`it` itself already says it is an iterator. But when a function's return
type determines the caller's behavior — for example, whether what comes
back is a reference or a copy — hiding this information behind `auto`
pushes the reader toward a wrong assumption. As this lesson has shown,
the difference between `auto` and `auto&` is, for the compiler, a
visible `&` mark, but for the reader, noticing that mark is the only way
to guess whether the source in the code stays **unchanged** or becomes
**changed**. This is why writing `const auto&` in a read-only variable,
`auto&` in a loop that modifies elements, and skipping `auto` altogether
to write out the actual type when that type itself carries meaning,
preserves readability without giving up the shortcut.

M11/K04 ("C++ Fundamentals and Object Design") measured **which object**
an expression works with — the `copy` kind there named a **target**, the
new object an assignment or a constructor writes into. This course's
`unchanged` kind names something entirely different: an operation's
**source**, that is, the object that already exists and is being worked
on. When `for (auto x : v)` builds a copy, there is a single operation
taking place; K04 would ask about that copy's target, this lesson asks
what the operation leaves in the source. The same line gives two
different answers to two different questions.

## The Bounding Measurement

What this lesson measures is what the deduced type leaves in the source
during container traversal. We traverse the same vector with two
different loops; the only difference in the heading is a single `&`
mark.

```cpp
// auto-reference-writing.cpp — does the auto& loop change the source
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v{5, 7};
    auto before = v;

    for (auto& x : v) x *= 2;

    std::printf("after auto&:");
    for (int value : v) std::printf(" %d", value);
    std::printf("\n");
    std::printf("did it change: %s\n", v == before ? "no" : "yes");
    return 0;
}
```

```text
after auto&: 10 14
did it change: yes
```

```cpp
// auto-copy-reading.cpp — auto builds a copy, the source does not change
#include <cstdio>
#include <vector>

int main() {
    std::vector<int> v{5, 7};
    auto before = v;
    int total = 0;

    for (auto x : v) {
        total += x;
        x *= 2;   // changes only the copy, does not reach v
    }

    std::printf("auto copy:");
    for (int value : v) std::printf(" %d", value);
    std::printf(", total %d\n", total);
    std::printf("did it change: %s\n", v == before ? "no" : "yes");
    return 0;
}
```

```text
auto copy: 5 7, total 12
did it change: no
```

**SL41.** `for (auto& x : v) x *= 2;` binds to each element through the
source itself; `x *= 2` writes directly to `v`, and `v` **changed** by
the end of the loop. `for (auto x : v)`, in turn, builds a copy at every
step; `x *= 2` changes only that copy, and `v` stays **unchanged** at the
end of the loop — even though `total` was computed from the copies,
nothing was written to the source. The kind of the type deduction is
determined here by a single character in the heading: the compiler sees
the `&` and decides whether to build a reference or a copy, and the
reader has to infer this from the rest of the line. This lesson's kind is
named by the result the `auto&` form gives: **changed**.

## Summary

- The kind of this lesson's central measurement is **changed**:
  `for (auto& x : v) x *= 2;` writes to the source itself, while
  `for (auto x : v)` changes only a copy and leaves the source
  **unchanged**.
- `auto` drops an expression's const and reference; `auto&` does not
  drop the reference, and `const auto&` reads without taking a copy.
- `auto&&` is a forwarding reference: it deduces `T&` if the initializer
  is an lvalue, `T&&` if it is an rvalue; this behavior appears only in
  deduced contexts.
- `decltype` gives an expression's type exactly as it is, without
  dropping the reference; `decltype(auto)` deduces a function's return
  type by the same rule, and wrapping the return expression in
  parentheses can turn the result from a value into a reference.
- In structured bindings, `auto [name, count]` leaves the source
  **unchanged**, `auto& [name, count]` **changes** the source — again,
  the difference is the single `&` mark before the square brackets.
- Writing out a type name is justified when that name tells the reader
  something about the source's behavior; otherwise, the `auto` shortcut
  does not hurt readability.

## Course Wrap-Up

Throughout this course we measured eleven different operations with the
same question: what does an operation leave in the source? The table
below summarizes each lesson's measured operation, what is left in the
source, and that lesson's bounding measurement.

| Lesson | Operation measured | Left in the source | Bounding measurement |
| --- | --- | --- | --- |
| Smart Pointers | `unique_ptr` moved | empty | `std::move` alone does not move; the source stays **unchanged** unless the move constructor or move assignment is actually invoked |
| Move Semantics | `std::string` moved | unspecified | `std::move` on a `const` object falls back to the copy constructor, the source stays **unchanged** |
| The Rule of Zero and the Rule of Five | `std::move` on a class with a destructor | unchanged | The same line falls into two different kinds depending on how the class is written: **unchanged** or **unspecified** |
| Non-Copyable and Non-Movable Types | Its own move constructor | empty | `is_move_constructible_v<Lock>` is **false** — the copy constructor is deleted, the move was never declared at all |
| Containers | Reading with `operator[]` on a map | changed | `operator[]` does not compile on a `const map`; it demands write permission even to read |
| Iterators and Validity | An erased iterator | unspecified | The same erase on `std::list` invalidates only the erased node's iterator, the others stay **unchanged** |
| Algorithms | `std::sort` | changed | Both `sort` and `stable_sort` are **changed**, but the order of equal elements is guaranteed in only one |
| The Erase–Remove Idiom | The tail after `std::remove` | unspecified | C++20's free `std::erase` moves directly to **changed** without ever touching the tail |
| Lambdas and Function Objects | Capture by value with `mutable` | unchanged | The same body gives three kinds with three different captures: value **unchanged**, reference **changed**, move **unspecified** |
| Ranges | A view from an lvalue | unchanged | A view constructed from an rvalue turns the source **empty** through the move constructor |
| Type Deduction | A loop with `auto&` | changed | The single `&` between `auto` and `auto&` turns the source from **unchanged** to **changed** |

**3 unchanged · 3 changed · 2 empty · 3 unspecified; 8 with a stated
state, 3 without.** This distribution is the distribution of the eleven
lessons in the table, and it is identical to the distribution of the
core established at the start of the course: every operation either
leaves the source as it is, changes it, empties it by contract, or
leaves it without stating its value, and that value can only be looked
at after it has been given a new one.

The next course is **Templates and Performance**; it covers templates,
concurrency, and performance topics.
