Let the compiler figure out the type. It's almost always right — and when it isn't, knowing why is the key to mastery.
Consider this line of C++98 code:
c++98 std::map<std::string, std::vector<int>>::const_iterator it = myMap.cbegin();
Now the C++11 version:
c++11 auto it = myMap.cbegin();
Same meaning. One tenth the typing. Zero chance of a typo in the type name. This is the surface argument for auto, but Meyers goes much deeper. auto doesn't just save keystrokes — it prevents bugs, avoids implicit conversions, and in some cases generates faster code.
Common declarations. The orange bars show explicit type length, teal shows auto length.
Critics say auto hurts readability because you can't see the type. Meyers counters: the variable name should tell you what it is, and the initialization expression tells you the type. The explicit type is often redundant noise.
c++ // Explicit: type is stated twice (redundant) Widget w = getWidget(); // auto: type stated once, by the expression auto w = getWidget();
More importantly, auto variables must be initialized. You can't write auto x; — the compiler rejects it. This eliminates an entire class of uninitialized-variable bugs.
Closures (lambda types) are another win. The type of a lambda is an unnamed class that only the compiler knows. Before auto, you had to use std::function to hold a lambda — which has overhead. With auto, you get the exact closure type, zero overhead:
c++ // std::function: type erasure, heap allocation possible std::function<bool(int,int)> cmp = [](int a, int b){ return a < b; }; // auto: exact lambda type, no overhead auto cmp = [](int a, int b){ return a < b; };
Here's a subtle bug that explicit types introduce. You want the size of a vector:
c++ std::vector<int> v; unsigned sz = v.size(); // Bug on 64-bit systems!
v.size() returns std::vector<int>::size_type, which is typically size_t — a 64-bit type on 64-bit systems. But unsigned is 32 bits. If the vector has more than 232 elements, the value silently truncates. With auto, the right type is deduced:
c++ auto sz = v.size(); // Always the correct type
An even nastier example involves iterating over a std::unordered_map:
c++ std::unordered_map<std::string, int> m; // Bug: key should be const std::string, not std::string! for (const std::pair<std::string, int>& p : m) { ... } // The map's value_type is pair<const string, int> // So the explicit type causes a COPY of every element!
pair<string, int> doesn't match the actual type pair<const string, int>, the compiler silently creates a temporary and binds the reference to that. Every iteration copies the key. auto gets this right for free.const pair<string,int>& p for iterating over an unordered_map?We saw that std::function has overhead compared to auto for lambda types. Let's understand why.
A std::function object uses type erasure: it wraps any callable behind a uniform interface. This requires storing the callable (possibly on the heap), and calling through a virtual dispatch or function pointer. An auto variable holding a lambda has the exact closure type — the compiler can inline the call.
Compare memory layout for storing a simple lambda.
| Property | auto (closure) | std::function |
|---|---|---|
| Size | Exact closure size (often 1 byte) | Fixed size (~32-64 bytes), may heap-allocate |
| Call overhead | Inline-able | Indirect call through function pointer |
| Heap allocation | Never | Possible for large closures |
| Type | Unique unnamed type | Type-erased std::function<sig> |
Item 6 is where Meyers warns that auto isn't always perfect. Some C++ APIs return proxy objects — objects that stand in for the real type but behave differently. The most notorious example is std::vector<bool>.
c++ std::vector<bool> features(const Widget& w); Widget w; bool highPriority = features(w)[5]; // OK: implicit conversion auto highPriority = features(w)[5]; // BUG: auto deduces // std::vector<bool>::reference!
The problem: vector<bool>::operator[] doesn't return bool& like every other vector. It returns a proxy object std::vector<bool>::reference that implicitly converts to bool. With an explicit bool type, the conversion happens automatically. With auto, you get the proxy itself — which may hold a dangling pointer to a temporary.
reference object holds a pointer into the temporary vector<bool> returned by features(w). That temporary is destroyed at the semicolon. Now highPriority holds a dangling reference to freed memory. Undefined behavior.Other proxy types in the wild: expression templates (Eigen, Blaze), Matrix::operator[] in some math libraries, and std::bitset::reference.
auto highPriority = features(w)[5]; cause undefined behavior?The fix for the proxy trap is simple: use auto but explicitly cast the initializer to the type you want:
c++ // The fix: static_cast forces the conversion auto highPriority = static_cast<bool>(features(w)[5]); // General pattern: auto val = static_cast<DesiredType>(expression_returning_proxy);
This keeps the auto (so you still benefit from forced initialization and terseness) while making your type intention explicit where auto would deduce a proxy. It's also a signal to future readers: "I'm deliberately forcing a type conversion here."
Given a declaration, should you use auto or an explicit type? This interactive tool presents real-world scenarios and walks through the reasoning.
Click each scenario to see whether auto is safe and why.
auto is one of the most impactful features of modern C++. The two items in this chapter — prefer auto (Item 5) and the typed initializer idiom (Item 6) — form a simple but powerful discipline.
| Item | Key Takeaway |
|---|---|
| Item 5 | Prefer auto: forced initialization, avoids implicit conversions, less typing, no type mismatch bugs. |
| Item 6 | When auto deduces a proxy type, use static_cast<T>() to force the desired type. |
"The type declared on the left is for the human. The type deduced from the right is for the compiler. When they disagree, the compiler wins — so let it write the type in the first place." — paraphrasing Meyers