Learn C++ STL

Lesson 4 of 9 · From C to Just-Enough C++

Module 1 · From C to Just-Enough C++

auto and the Range-for: Less Typing, Same Meaning

FreeReading

In this lesson

  • Write auto to let the compiler fill in a variable's type, and make GCC 12 tell you which type it chose.
  • Explain what auto drops, const and references, and keep them with auto& and const auto&.
  • Write the three forms of the range-for and choose the right one for copying, changing and reading.

Zara found this line in an old contest solution: for (std::vector<std::pair<std::string, int>>::iterator it = v.begin(); it != v.end(); ++it). It visits every item of a list, and the type alone is longer than most of her programs. Today's C++ writes the same loop as for (auto& p : v). This lesson explains the two pieces that make it that short: auto and the range-for.

auto: the compiler writes the type

When a variable starts with a value, the value already says what type it is. 5 is an int, 2.5 is a double, 'z' is a char. The keyword auto asks the compiler to take the type from the value, so you do not write it twice.

A variable with auto

auto name = initial_value;
  • auto stands where the type would be. The compiler replaces it with the type of initial_value.
  • name is an ordinary variable of that type from then on.
  • = initial_value is required: with nothing to look at, there is no type to take.
#include <iostream>
#include <string>

int main()
{
    auto count = 3;
    auto price = 2.5;
    auto grade = 'A';
    auto name = std::string("Zara");

    std::cout << name << " bought " << count << " pens at " << price << '\n';
    std::cout << "grade " << grade << ", total " << count * price << '\n';
    return 0;
}
Zara bought 3 pens at 2.5
grade A, total 7.5

count is an int, price a double, grade a char and name a std::string. The type is chosen when the program is compiled, and it never changes after that. So auto is not a box that holds "anything": it is the same fixed type you would have typed, filled in for you.

Making the compiler say which type it chose

Maria wants proof, not a promise. There is an old trick for that: ask the compiler to do something impossible with the variable, and read the error. The message has to name the type.

The impossible thing here is turning a value into an empty struct, a struct with no fields, which C++ allows. GCC 12 refuses each line, and each refusal says what it was given.

struct Probe {};

int main()
{
    auto a = 5;
    auto b = 2.5;
    auto c = 'z';
    auto d = 3000000000;
    auto e = "hi";
    auto f = 7 / 2;

    Probe p1 = a;
    Probe p2 = b;
    Probe p3 = c;
    Probe p4 = d;
    Probe p5 = e;
    Probe p6 = f;
    return 0;
}

On the Playground's command line, GCC 12 prints six errors. Here is the part of each line that matters.

DeclarationGCC 12 saysSo the type is
auto a = 5;conversion from 'int' to non-scalar type 'Probe' requestedint
auto b = 2.5;conversion from 'double' ...double
auto c = 'z';conversion from 'char' ...char
auto d = 3000000000;conversion from 'long int' ...long: too big for an int
auto e = "hi";conversion from 'const char*' ...a C string, not std::string
auto f = 7 / 2;conversion from 'int' ...int, holding 3

Two rows deserve a second look. 3000000000 does not fit an int, so the literal itself is a long; on the Playground's Linux a long is 8 bytes. And "hi" is still the C string you know, a const char*. That is why the first program wrote std::string("Zara").

So auto takes exactly the type of the value, with no guessing about what you meant, and one deliberate error lets the compiler show you.

What auto drops, and how to keep it

auto takes the type of the value, not everything about the variable it came from. Two things are left behind: const, and being a reference. The result is always a fresh, writable copy.

#include <iostream>

int main()
{
    const int limit = 10;
    auto a = limit;
    a = 11;

    int x = 5;
    int& r = x;
    auto b = r;
    b = 99;
    std::cout << "a = " << a << ", x after b = 99: " << x << '\n';

    auto& c = r;
    c = 42;
    std::cout << "x after c = 42: " << x << '\n';
    return 0;
}
a = 11, x after b = 99: 5
x after c = 42: 42

a is a plain int, so a = 11 is allowed even though limit was const. b is a copy of the value behind r, so changing it leaves x at 5. Only auto& kept the reference, and then c = 42 reached x.

You writeThe type becomesWhat you get
auto a = limit;inta writable copy; the const is dropped
auto b = r;inta copy; the reference is dropped
auto& c = r;int&a second name for x (lesson 03)
const auto& d = x;const int&a read-only second name, no copy

So plain auto always makes a copy. Add & for a second name, and const auto& for a read-only one.

The range-for: every box, in order

In C, walking an array means an index, a bound and an increment, three chances for Bob's off-by-one error. The range-for says only "for each element of this". It starts at the first box, ends after the last, and needs no index at all.

The range-for

for (declaration : range) {
    body
}
  • range is what to walk: an array whose size the compiler knows, or a container such as a vector.
  • declaration names one element at a time, such as int x, int& x or const auto& x.
  • body runs once per element, first to last.
#include <iostream>

int main()
{
    int marks[5] = {70, 85, 62, 91, 48};
    int total = 0;

    for (int m : marks) {
        total += m;
    }
    std::cout << "total " << total << ", average " << total / 5.0 << '\n';
    return 0;
}
total 356, average 71.2

There is one thing a C reader must know. A range-for over a C array walks every box of the array: its capacity, not its count. An int a[100] holding 5 marks gets 100 passes, 95 of them over unused boxes. So use it on a C array only when every box holds data, as above. Module 2's vector knows its own count, which is why the STL lessons use the range-for everywhere.

So the range-for visits each element once, from the first to the last, and there is no index to get wrong.

Three forms: copy, change, read

The declaration before the colon decides what each pass gets. It is lesson 03 again, and this picture holds all three forms.

Three range-for forms over the same array for (int x : marks) copy: x is its own box 70 85 62 copied 70 x for (int& x : marks) change: x is the box itself 70 x 85 62 x = 0 writes into marks for (const auto& x : marks) read: the box, read-only 70 x (read-only) 85 62 no copy, no writing

Here are the three forms in one program, each over the same marks.

#include <iostream>

int main()
{
    int marks[3] = {70, 85, 62};

    for (int x : marks) {
        x = 0;
    }
    std::cout << "after the copy form:   " << marks[0] << ' ' << marks[1] << ' ' << marks[2] << '\n';

    for (int& x : marks) {
        x *= 2;
    }
    std::cout << "after the change form: " << marks[0] << ' ' << marks[1] << ' ' << marks[2] << '\n';

    int best = 0;
    for (const auto& x : marks) {
        if (x > best) {
            best = x;
        }
    }
    std::cout << "best, by the read form: " << best << '\n';
    return 0;
}
after the copy form:   70 85 62
after the change form: 140 170 124
best, by the read form: 170

Step through the change form. On each pass x names the next box, and the array changes under it.

Pass 1: x names marks[0], and marks becomes 140 85 62. Pass 2: x names marks[1], 140 170 62. Pass 3: x names marks[2], 140 170 124.

The rule a working programmer uses: auto x when you want a copy to play with, auto& x when you change the elements, and const auto& x when you only read them. For small numbers the copy costs nothing; for anything bigger, the copy is a real cost, measured below.

In C++20

A range-for can start with its own small declaration, the way a C for does, so a counter lives inside the loop and nowhere else. At -std=c++17, GCC 12 accepts it only with a warning that it belongs to C++20. So this program carries the C++20 marker. The Run button below opens the Playground on C++20.

#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> words = {"tea", "biscuit", "jam"};
    for (int i = 0; const auto& w : words) {
        std::cout << i << ": " << w << '\n';
        i++;
    }
    return 0;
}
0: tea
1: biscuit
2: jam
Run in Compiler

The range-for on the containers you met in Module 0

The same loop works on every STL container, which is the whole point. Here is one program each on two containers that later modules teach properly. A std::vector is an array that knows its own size (Module 2). A std::map keeps pairs of a key and a value in key order (Module 9).

#include <iostream>
#include <vector>

int main()
{
    std::vector<int> laps = {62, 58, 61, 57};
    int fastest = laps[0];

    for (int t : laps) {
        if (t < fastest) {
            fastest = t;
        }
    }
    std::cout << laps.size() << " laps, fastest " << fastest << " s\n";
    return 0;
}
4 laps, fastest 57 s
#include <iostream>
#include <map>
#include <string>

int main()
{
    std::map<std::string, int> stock = {{"tea", 12}, {"jam", 3}, {"bread", 7}};

    for (const auto& item : stock) {
        std::cout << item.first << ": " << item.second << '\n';
    }
    return 0;
}
bread: 7
jam: 3
tea: 12

The map printed its items in alphabetical order of the key, not in the order they were written, because a map keeps its keys sorted. Each item is a pair, and .first and .second are its two parts; lesson 06 teaches the pair. The element type here is long, std::pair<const std::string, int>, which is exactly where const auto& earns its keep.

So the range-for reads the same over an array, a vector and a map. Learn it once, and every container in this track opens with it.

The copy trap, measured

Kenji writes for (auto t : titles) because it is shortest. For a list of numbers that is fine. For a list of strings, each pass copies a whole string before the body even starts.

#include <chrono>
#include <iostream>
#include <string>
#include <vector>

int main()
{
    std::vector<std::string> titles(100000, std::string(100, 'x'));

    auto t0 = std::chrono::steady_clock::now();
    long long a = 0;
    for (int round = 0; round < 10; round++) {
        for (auto t : titles) {
            a += t.size();
        }
    }
    auto t1 = std::chrono::steady_clock::now();
    long long b = 0;
    for (int round = 0; round < 10; round++) {
        for (const auto& t : titles) {
            b += t.size();
        }
    }
    auto t2 = std::chrono::steady_clock::now();

    std::chrono::duration<double, std::milli> byCopy = t1 - t0;
    std::chrono::duration<double, std::milli> byRef = t2 - t1;
    std::cout << "auto t:        " << a << " characters in " << byCopy.count() << " ms\n";
    std::cout << "const auto& t: " << b << " characters in " << byRef.count() << " ms\n";
    return 0;
}

The list holds 100,000 strings of 100 characters, and each loop walks it 10 times. This is one run on Compiler Explorer, x86-64 GCC 12.2, at the Playground's flags, -O2 -std=c++17.

LoopCharacters countedTime
for (auto t : titles)10000000047.2 ms
for (const auto& t : titles)1000000001.8 ms

Same answer, 26 times slower in that run; three later runs gave 16 to 22 ms against about 1 ms, 15 to 24 times. All of the difference is copying: a million strings made and thrown away, to read a number each one already knew. So const auto& is the default for reading anything bigger than a number.

Where auto waits for later

auto can also stand for a function's return type, as in auto half(int n), and the compiler takes it from the return statement. This track does not use that yet. A reader looks at a function's first line to see what it gives back, so this module writes return types out. Module 12 uses auto once more, to hold a lambda, whose type cannot be written at all.

So in this module auto appears in two places only: a variable with a value, and the declaration of a range-for.

Example 1: the cost of a shopping basket

Alice adds up a basket. Every box of the array holds a real price, so a range-for over it is exactly right.

#include <iostream>

int main()
{
    double prices[4] = {1.5, 2.25, 0.75, 4.0};
    double total = 0;

    for (auto p : prices) {
        total += p;
    }
    std::cout << "basket: " << total << '\n';
    return 0;
}
basket: 8.5

auto p is a double, a copy of each price, and a copy of a number is free. Nothing in the array changes, so nothing more is needed.

Run in Compiler
Example 2: David converts the week's temperatures

David's weather log is in Celsius, and his cousin's phone shows Fahrenheit. The loop changes every reading where it stands, so it needs auto&.

#include <iostream>

int main()
{
    double temps[5] = {21.5, 25, 18, 30, -5};

    for (auto& t : temps) {
        t = t * 9 / 5 + 32;
    }
    std::cout << "Fahrenheit:";
    for (auto t : temps) {
        std::cout << ' ' << t;
    }
    std::cout << '\n';
    return 0;
}
Fahrenheit: 70.7 77 64.4 86 23

The first loop writes into the array through t; the second only prints copies. Remove the & from the first loop, and the program prints the Celsius values unchanged, with no message from GCC 12 at any command line.

Run in Compiler
Example 3: Amara's reading list by first letter

Amara keeps her reading list as one word per title. She wants the titles that start with a given letter, counted. The program reads the list into a vector, which knows its own size (Module 2), and reads it back with const auto&.

#include <iostream>
#include <string>
#include <vector>

int main()
{
    int n = 0;
    std::cin >> n;

    std::vector<std::string> titles(n);
    for (auto& t : titles) {
        std::cin >> t;
    }

    char letter = ' ';
    std::cin >> letter;

    int found = 0;
    for (const auto& t : titles) {
        if (t[0] == letter) {
            std::cout << t << '\n';
            found++;
        }
    }
    std::cout << found << " of " << titles.size() << " start with " << letter << '\n';
    return 0;
}
Dune
Dracula
2 of 5 start with D

That output is for the input 5, then Dune Emma Dracula Matilda Ulysses, then D. The first range-for fills the boxes, so it needs auto&, the way std::cin >> needs a box to write into. The second only reads, so const auto& skips five string copies. Zara would try a letter no title starts with; the program prints 0 of 5 start with Q.

Run in Compiler

Where this is used

  • The C++ Core Guidelines. Rule ES.11 says to use auto to avoid repeating type names, and ES.71 says to prefer a range-for to a plain for when there is a choice.
  • The LLVM coding standards. The rules for the LLVM compiler project have a section titled "Beware unnecessary copies with auto". It is the copy trap above, and it asks for auto& unless a copy is meant.
  • Google's C++ style guide. It allows type deduction where it makes code clearer to a reader new to the project. It warns against it where it hides a type that matters. Chromium's C++ style is built on that guide.
  • Every STL module after this one. Each container lesson walks its container with a range-for and const auto&, from Module 2's vector to Module 10's hash maps.

Common mistakes

1. Changing a copy in a range-for.

for (auto x : marks) {
    x *= 2;
}

No message at any command line, the Playground's or -Wall -Wextra. The program then prints 70 85 62, unchanged. Write auto& x. You will make this one because auto x is the form you read most often, and reading is fine with a copy.

2. auto with nothing to look at.

auto x;
x = 5;

An error at every command line: error: declaration of 'auto x' has no initializer. The type comes from the value, so the value must be on the same line. Write auto x = 5;, or write the type when the value comes later.

3. Letting 0 choose the type of a running total.

auto total = 0;
total += 2.5;
total += 2.5;

No message at any command line, and the total prints 4, not 5. 0 is an int, so total is an int for good, and each 2.5 is cut to fit: 2.5 becomes 2, then 4.5 becomes 4. Write double total = 0; or auto total = 0.0;. The same trap gives an int where a sum needs long long.

4. A range-for over an array that came in as a parameter.

void printAll(int a[])
{
    for (int x : a) {
        std::cout << x << ' ';
    }
}

An error at every command line: error: 'begin' was not declared in this scope; did you mean 'std::begin'?, and the same for end. The suggestion misleads: the real cause is that an array parameter is a pointer, as in C, and a pointer has no size to walk. Pass the size and use an index loop, or pass a vector (Module 2).

Brain teaser

Kenji writes three numbers in braces and lets auto decide.

#include <iostream>

int main()
{
    auto x = {1, 2, 3};
    std::cout << x.size() << '\n';
    return 0;
}

It compiles with no message and prints 3. So x is not an int, and it is not a C array either, because a C array has no .size(). What is it, and why does this track tell you to write the type yourself in this one place?

Use the trick from "Making the compiler say which type it chose" on x, and read the message.

Exercise 1Easy

A shop doubles every price for one silly day of the year, and Bob has to update the price list. He must change each price where it stands, not make a new list. The starter reads the prices into std::vector<long long> values, an array that knows its own size. Double every value in place with a range-for, then print the list.

Input. The first line holds n. The second holds n integers.

Output. One line with the n integers, each doubled, in the input order, separated by single spaces.

Constraints. 1 <= n <= 80000. Each integer is between -2000000000 and 2000000000.

Sample. Input 4 and 3 -1 0 2000000000 gives 6 -2 0 4000000000.

#include <iostream>
#include <vector>

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    int n = 0;
    std::cin >> n;

    // A vector is an array that knows its own size (Module 2 teaches it),
    // so a range-for over it walks exactly the n values.
    std::vector<long long> values(n);
    for (long long& x : values) {
        std::cin >> x;
    }

    // Double every value in place with a range-for. Then print the
    // values on one line, separated by single spaces.

    return 0;
}

Graded as doubled. The hidden tests include n = 1, a single -2000000000, and 80000 values of 2000000000, whose doubles do not fit an int. A loop that doubles a copy prints the input unchanged and fails every test but the one whose only value is 0.

Run in Compiler
Exercise 2Medium

Maria plays a word game and wants the longest word she has found. Print the longest word and its length. If several words share the longest length, print the first of them.

Input. A line with n, then n words separated by spaces.

Output. One line: the longest word, one space, and its length.

Constraints. 1 <= n <= 1000. Each word is 1 to 30 lowercase letters.

Sample. Input 4 and tea biscuit jam toffee gives biscuit 7.

#include <iostream>
#include <string>
#include <vector>

int main()
{
    int n = 0;
    std::cin >> n;

    // A vector of n strings (Module 2), read with a range-for.
    std::vector<std::string> words(n);
    for (auto& w : words) {
        std::cin >> w;
    }

    // Walk words with a range-for and keep the longest one.
    // A string's length is w.size().

    return 0;
}

Not graded on its own. Choose the declaration in your loop with this lesson's rule in mind: you only read the words.

Run in Compiler

Common doubts

  • Is auto like a variable in Python or JavaScript, which can hold anything?

    No. In those languages a variable can hold a number now and a string later. An auto variable gets one type when the program is compiled and keeps it. Mistake 3 is that rule biting: the total stayed an int.

  • Does auto make my program slower?

    No. The compiler writes in the same type you would have written, so the machine code is the same. The only cost is the one this lesson measured, and that comes from copying, not from auto.

  • When should I write the type myself?

    Write it when the value does not say what you mean. Examples are 0 for a total that needs long long or double, a C string where you want std::string, and the braces of the brain teaser. Also when the type is short and tells the reader something, such as int count = 0;.

  • How do I get the index inside a range-for?

    The range-for has none. Keep a counter beside it, as the "In C++20" block does, or use an index loop when the index is the point. Both are fine; choose the one that reads better.

  • Why can I write for (const auto& x : marks) when marks is not const?

    Adding const only takes a power away, so it is always allowed. The opposite is refused: on a const int marks[4], a loop that writes through auto& m gets error: assignment of read-only reference 'm' from GCC 12, because auto& keeps the const of what it names.

Key takeaways

  • auto x = value; gives x the type of the value, fixed at compile time; it is not "any type".
  • Assigning to an empty struct makes GCC 12 print the type auto chose.
  • Plain auto drops const and references and makes a copy; auto& and const auto& keep them.
  • A range-for visits every element once, first to last; over a C array that means every box, used or not.
  • auto x to copy, auto& x to change, const auto& x to read anything bigger than a number.

Next, lesson 05 explains the angle brackets in std::vector<long long>: one template, filled in with a type.

End of lesson 4

Mark it done, and your progress moves with you.

Next: Templates, the Idea: One Function, Every Type