C++ / FUNDAMENTAL TYPES AND VARIABLES
The auto keyword and when deduction helps
Predict what auto deduces from an initialiser and pick auto, auto&, const auto&, or auto* so you get a copy or an alias on purpose.
What you will learn
- Read auto x = e; as by-value template deduction: & and top-level const vanish.
- Pick auto&, const auto&, or auto* deliberately when you need an alias, not a copy.
- Store lambdas and iterators in auto because their types are unspellable or noisy.
- Spot the hidden copy in for (auto p : map) and fix it with const auto&.
Understanding The auto keyword and when deduction helps
The keyword auto does not introduce a new kind of variable; it tells the compiler to work out the type from the initialiser and then fix it for good. The rule it uses is the one a function template uses for a by-value parameter: auto x = e; gives x the type that template <class T> void f(T); would deduce for T when called with e. That model explains the restrictions you will hit, such as auto always needing an initialiser and auto a = 1, b = 2.0; being rejected because one declaration cannot deduce two different types.
By-value deduction strips references and top-level const, and makes arrays and functions decay to pointers. So when e has type const std::string&, auto x = e; produces a fresh, modifiable std::string copied from the original, and writes to x never reach the original object. If you want the reference, you write the reference: auto& x = e; for read-write access to the original, const auto& x = e; to inspect a large object without copying it. Const that is not top-level survives untouched, so deducing from a const int* still gives you a const int*.
Deduction earns its place in three situations: the type has no name you can write, as with a lambda's closure type; the name is long incidental noise that would have to be edited if the container changed, as with std::map<std::string, std::vector<int>>::const_iterator; or writing the type by hand risks getting it subtly wrong. The third case is the sneakiest, because the element type of std::map<std::string, int> is std::pair<const std::string, int>, so a hand-written const std::pair<std::string, int>& in a range-for binds to a temporary copy of every element instead of the element itself. Deduction hurts in the opposite case, when the initialiser is a bare function call whose return type the reader cannot guess; there the type name is the documentation, so spell it out.
<iostream>
<type_traits>
<vector>
int main() {
std::vector<int> v{10, 20, 30};
const int& second = v[1];
auto copy = second; // int: the & and the const are dropped
const auto& alias = v[1]; // const int&: nothing is copied
copy = 99; // fine, copy is an independent int
std::cout << std::boolalpha
<< "decltype(copy) is int: "
<< std::is_same_v<decltype(copy), int> << '\n'
<< "decltype(alias) is const int&: "
<< std::is_same_v<decltype(alias), const int&> << '\n'
<< "v[1] is still: " << alias << '\n';
long sum = 0;
for (auto x : v) { x *= 2; sum += x; } // x is a copy of each element
for (auto& x : v) { x += 1; } // x aliases each element
std::cout << "sum of doubled copies: " << sum << '\n';
std::cout << "v:";
for (const auto& x : v) { std::cout << ' ' << x; }
std::cout << '\n';
}
auto is not a type but a deduction rule borrowed from templates: it takes the type of the initialiser and throws away references and top-level const, so plain auto always means a copy.
Worked examples
Types you cannot or should not spell
Shows the cases where deduction is the better choice: an iterator, a lambda, and a map element.
<iostream>
<map>
<string>
int main() {
std::map<std::string, int> ages{{"ada", 36}, {"grace", 45}};
auto it = ages.find("ada");
if (it != ages.end()) {
std::cout << it->first << " is " << it->second << '\n';
}
auto twice = [](int n) { return n * 2; };
std::cout << "twice(21) = " << twice(21) << '\n';
for (const auto& [name, age] : ages) {
std::cout << name << " -> " << age << '\n';
}
}
Example explained
Line 1auto it stands in for std::map<std::string, int>::iterator, and it keeps working if the container type is later changed.
Line 2The lambda has a unique unnamed class type invented by the compiler, so auto is the only way to give it a name.
Line 3The map's element type is std::pair<const std::string, int>; const auto& binds straight to the element, whereas a hand-written const std::pair<std::string, int>& would build a temporary copy each iteration.
Line 4The structured binding [name, age] needs C++17 and splits the pair without ever naming its type.
The dropped reference is observable
Demonstrates that auto copies where auto& aliases, using a member function that returns a reference.
<iostream>
<string>
struct Config {
std::string name = "default";
std::string& label() { return name; }
};
int main() {
Config c;
auto copy = c.label(); // deduced std::string: a copy of name
copy = "changed copy";
auto& ref = c.label(); // deduced std::string&: an alias for name
ref = "changed ref";
std::cout << "copy: " << copy << '\n';
std::cout << "c.name: " << c.name << '\n';
}
Example explained
Line 1c.label() has type std::string&, but auto copy deduces plain std::string, so the assignment lands in a detached copy.
Line 2auto& ref keeps the reference, so assigning through ref writes into c.name itself.
Line 3Nothing on the line auto copy = c.label(); looks like a copy, which is why the deduction rule has to be memorised rather than inferred from the syntax.
Important notes
auto s = "hello"; deduces const char*, not std::string, because the literal is a char array that decays to a pointer; write std::string s = "hello"; if you want a string object.
Braces have a special deduction rule with auto: auto x = {1, 2}; gives std::initializer_list<int>, while auto x{1}; gives int and auto x{1, 2}; is rejected.
Common mistakes
Expecting auto to alias the original: for (auto s : names) s += "!"; compiles, runs, copies every string, and leaves names unchanged; only auto& modifies the elements.
Treating auto as dynamic typing: auto n = 0; n = 2.9; keeps n an int and stores 2, and auto n; on its own is a compile error because there is nothing to deduce from.
Expecting auto to choose a sensible type: auto ratio = 3 / 4; deduces int from an integer division, so ratio is 0; auto reports the expression's type, it never improves it.
Try it yourself
Change, predict, then run
In a browser editor, build a std::vector<std::string> of three words, then run one loop written as for (auto w : words) w += "!"; and another as for (auto& w : words) w += "!";, printing the whole vector after each loop. Report which exclamation marks survived and why.
Open the C++ workspaceCheck your understanding
A library declares const std::string& title();. What type does t have in auto t = title();, and why?
- const std::string&, because auto reproduces the type of the initialiser expression exactly
- std::string, because by-value deduction removes the reference and then the top-level const
- const std::string, because the reference is dropped but const is part of the type
- std::string&, because a reference cannot be copied so it has to stay a reference
Show answer
auto follows the rules for a by-value template parameter: the reference is removed first, then the top-level const, leaving an independent std::string that owns its own copy of the characters and can be reassigned. The first option is tempting because the expression really does have type const std::string&, but auto never deduces a reference on its own; you have to write the ampersand yourself with auto& or const auto&.