Lesson 11 / 11
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.
Published Updated
In this lesson
- 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.
Contents
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
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.
// 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;
}
after auto&: 10 14 did it change: yes
// 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;
}
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, whilefor (auto x : v)changes only a copy and leaves the source unchanged. autodrops an expression’s const and reference;auto&does not drop the reference, andconst auto&reads without taking a copy.auto&&is a forwarding reference: it deducesT&if the initializer is an lvalue,T&&if it is an rvalue; this behavior appears only in deduced contexts.decltypegives 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
autoshortcut 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.
To keep your progress and take notes, Log in
My notes
Log in to take notes.