Skip to content
academia.sh

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

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close