Module 1 · From C to Just-Enough C++
auto and the Range-for: Less Typing, Same Meaning
In this lesson
- Write
autoto let the compiler fill in a variable's type, and make GCC 12 tell you which type it chose. - Explain what
autodrops,constand references, and keep them withauto&andconst 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;
autostands where the type would be. The compiler replaces it with the type ofinitial_value.nameis an ordinary variable of that type from then on.= initial_valueis 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.
| Declaration | GCC 12 says | So the type is |
|---|---|---|
auto a = 5; | conversion from 'int' to non-scalar type 'Probe' requested | int |
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 write | The type becomes | What you get |
|---|---|---|
auto a = limit; | int | a writable copy; the const is dropped |
auto b = r; | int | a 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
}
rangeis what to walk: an array whose size the compiler knows, or a container such as a vector.declarationnames one element at a time, such asint x,int& xorconst auto& x.bodyruns 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.
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.
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
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.
| Loop | Characters counted | Time |
|---|---|---|
for (auto t : titles) | 100000000 | 47.2 ms |
for (const auto& t : titles) | 100000000 | 1.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.
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.
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.
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.
Where this is used
- The C++ Core Guidelines. Rule ES.11 says to use
autoto avoid repeating type names, and ES.71 says to prefer a range-for to a plainforwhen 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).
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.
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 CompilerCommon doubts
Is
autolike 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
autovariable gets one type when the program is compiled and keeps it. Mistake 3 is that rule biting: the total stayed anint.Does
automake 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
0for a total that needslong longordouble, a C string where you wantstd::string, and the braces of the brain teaser. Also when the type is short and tells the reader something, such asint 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)whenmarksis notconst?Adding
constonly takes a power away, so it is always allowed. The opposite is refused: on aconst int marks[4], a loop that writes throughauto& mgetserror: assignment of read-only reference 'm'from GCC 12, becauseauto&keeps theconstof what it names.
Key takeaways
auto x = value;givesxthe type of the value, fixed at compile time; it is not "any type".- Assigning to an empty
structmakes GCC 12 print the typeautochose. - Plain
autodropsconstand references and makes a copy;auto&andconst 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 xto copy,auto& xto change,const auto& xto 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