[[auto]] and the range-based [[for]] simplify C++ in two ways: [[auto]] lets the compiler name the type for you, and the range-based loop walks a container without anybody writing an iterator. Between them they remove a great deal of visual noise from modern C++ — but both are tools, and using them well means knowing when to spell the type out.
auto names the type for you
Before C++11, a variable's type had to be written out, which was painful for types with long names.
#include
#include
#include
#include
int main() {
std::map> readings;
readings["oil"] = {42, 43, 44};
for (const std::pair>& entry : readings) {
std::cout << entry.first << ": " << entry.second.size() << " values\n";
}
return 0;
}The output shows the nested type being spelled out twice:
oil: 3 valuesWith auto the type is deduced from the initialiser.
#include
#include
#include
#include
int main() {
std::map> readings;
readings["oil"] = {42, 43, 44};
for (const auto& entry : readings) {
std::cout << entry.first << ": " << entry.second.size() << " values\n";
}
return 0;
}The output is identical, and the declaration is far easier to read. The & matters: without it the pair is copied on every iteration, and without const you could not call a const member on it.
The three forms of auto
There are three ways to write auto, and they differ in what they deduce and whether they copy.
auto value = expression;— takes a copy, dropping references and const.auto& value = expression;— binds a reference to the original.const auto& value = expression;— binds a const reference, which is what you usually want.
#include
#include
int main() {
std::string original = "measured";
auto copy = original;
auto& reference = original;
const auto& const_reference = original;
copy += " (copy)";
reference += " (reference)";
// const_reference += " (ignored)"; // does not compile: it is const
std::cout << "original: " << original << '\n';
std::cout << "copy: " << copy << '\n';
std::cout << "reference: " << reference << '\n';
std::cout << "const_reference: " << const_reference << '\n';
return 0;
}The output shows exactly what each form did:
original: measured (reference)
copy: measured (copy)
reference: measured (reference)
const_reference: measured (reference)The copy left the original alone, the reference changed it, and the const reference refused to.
The range-based for loop
The range-based for walks any container that provides .begin() and .end().
#include
#include
#include
#include
int main() {
std::vector parcels = {"p-001", "p-002", "p-003"};
for (const std::string& parcel : parcels) {
std::cout << parcel << ' ';
}
std::cout << '\n';
std::map counts = {{"north", 3}, {"south", 5}};
for (const auto& [zone, count] : counts) {
std::cout << zone << '=' << count << ' ';
}
std::cout << '\n';
return 0;
}The output shows both loops working, and the second one using a structured binding to name the key and the value:
p-001 p-002 p-003
north=3 south=5Because the loop only needs .begin() and .end(), it works with vectors, lists, maps, sets, arrays and any type that behaves like them.
What auto is good for
- Long type names — the pair in a map, the return type of a factory function, the type of a complex iterator.
- Iterators —
for (auto it = container.begin(); it != container.end(); ++it). - Lambda parameters — the parameter types are deduced.
- Uniform code — a template that handles any element type reads the same as one that handles a fixed type.
What auto is bad for
- Public interfaces. A function whose return type is
autohides the type from the caller. Spell out return types in headers. - When the type is not obvious.
auto value = 3.14;is fine, butauto result = process(data);tells the reader nothing. - When it changes the semantics.
autoon a function parameter decays a reference to a value, which can silently add a copy. - When you need a specific type.
auto* ptr = ...deduces a pointer, butautoalone would deduce a pointer *to pointer* in some contexts, so be explicit.
decltype(auto)
decltype(auto) deduces the type the way decltype would, which means it preserves references and top-level const.
#include
#include
#include
std::vector parcels() {
return {"p-001", "p-002"};
}
int main() {
const auto& safe = parcels(); // a reference to a temporary, extended
decltype(auto) by_value = parcels(); // a copy of the vector
std::cout << "safe size: " << safe.size() << '\n';
std::cout << "by value: " << by_value.size() << '\n';
return 0;
}The output shows both holding the same two elements:
safe size: 2
by value: 2decltype(auto) is most useful in forwarding functions, where you want to preserve whatever reference-ness the caller passed. Used carelessly it can dangle, because it may deduce a reference to a temporary.
Quick reference
auto value = expr;copies;auto&binds a reference;const auto&is the default choice.- The range-based
forneeds only.begin()and.end(). - Prefer
const auto&in a range-based loop to avoid copies. - Structured bindings name the parts of a pair, tuple or map entry.
- Spell out return types in public headers; use
autoin local code. decltype(auto)preserves the exact type, including references.
One more habit: reserve [[auto]] for local variables and template arguments. A public function signature is a promise, and a promise whose type the reader has to infer from the body is a promise that is hard to keep.
Try it yourself: write a loop over a [[std::map]] of parcel identifiers to weights, first with the full pair type spelled out, then with [[auto]]. Next, change the loop to use a structured binding. Then change [[const auto&]] to [[auto]] and watch the copies happen, using a large string type to make the difference visible.