C++ Range-Based for: auto, References, Temporaries, Structured Bindings — Practical Guide
이 글의 핵심
How to choose auto, auto&, and const auto& in range-for; pitfalls with temporaries and proxy iterators; pairing with C++17 structured bindings; custom begin/end; and practical patterns.
What is range-based for?
Range-based for (range-based for, C++11) is syntax for walking an entire sequence without writing indices or iterators by hand: you take one element at a time from a range.
std::vector<int> v = {1, 2, 3};
for (int x : v) {
std::cout << x << '\n';
}
Roughly, the loop uses iterators from begin(v) / end(v), and at each step the result of dereferencing is assigned to the loop variable.
for (auto&& __range = (v); ; ) {
auto __begin = begin(__range);
auto __end = end(__range);
for (; __begin != __end; ++__begin) {
int x = *__begin; // depends on the declaration form
// ...
}
}
The exact rules follow the standard’s “range-based for statement” clause. It pairs well with the general loop guide.
auto vs auto& vs const auto&
auto (by value)
Creates a copy of each element. Mutating x does not change the underlying container. That is cheap for small types like int and double.
for (auto x : vec) {
x *= 2; // elements of vec are unchanged
}
auto& (non-const reference)
An alias to the element. Mutations affect the original. A const container or const elements may make this ill-formed.
for (auto& x : vec) {
x *= 2; // elements of vec change
}
const auto& (const reference)
Widely used for read-only access without copying. Temporaries can be bound safely because lifetime extends to the loop body.
for (const auto& s : get_strings()) {
std::cout << s; // OK even if get_strings() returns a temporary
}
Choosing a form
| Goal | Suggestion |
|---|---|
| Read-only, large type | const auto& |
| Mutate elements | auto& (non-const range) |
| Cheap copy semantics | auto (small POD-like types) |
| Forwarding / generic signatures | auto&& (common in template code) |
auto&&: As a forwarding reference, it binds according to the range’s value_type and reference collapsing for lvalues vs rvalues. Template libraries use this often. |
for (auto&& e : container) {
// e binds as lvalue ref or rvalue ref
}
Temporary objects
When the range expression is a temporary
Under C++11 and later, if the range expression is a temporary, its lifetime is extended for the entire loop. So the following is safe:
for (const auto& x : make_vector()) { /* ....*/ }
What usually bites is not “nested temporaries” in the abstract, but proxy iterators and invalidation.
vector<bool> and proxy references
The std::vector<bool> specialization may yield something other than a real bool&. Using auto& and mutating through the proxy often works, but generic code that assumes std::vector<T>::reference is T& can break when T is bool.
Invalidation
If you reallocate or insert in the container during iteration, iterators break. Range-based for uses iterators internally, so the same rules apply.
Bad pattern (reference outlives the range)
const std::string* p = nullptr;
{
std::vector<std::string> v = {"a"};
for (const auto& s : v) {
p = &s; // do not use p outside the loop
}
} // v destroyed
// *p // undefined behavior
Lifetime extension for a temporary range is only guaranteed inside that for statement; escaping a pointer/reference to elements past the loop is still unsafe.
Structured bindings (C++17)
With structured bindings, you can unpack pair, tuple, map::value_type, and similar types in one step while iterating.
std::map<int, std::string> m;
for (const auto& [key, val] : m) {
std::cout << key << ": " << val << '\n';
}
Caution: iterating a std::map with auto& [k, v] yields std::pair<const Key, T>; the key is often not meant to be modified—if that is not what you want, const auto& [k, v] is safer.
std::vector<std::pair<int, int>> pairs = {{1,2},{3,4}};
for (auto [a, b] : pairs) { // copy
std::cout << a << b;
}
for (auto& [a, b] : pairs) { // references; can mutate
++a;
}
You can combine this with C arrays and struct members in the same style.
Custom types: begin / end
Range-based for finds begin / end via ADL (argument-dependent lookup). It works if:
std::begin(x)/std::end(x)are valid, orx.begin()/x.end()exist, or- Non-member
begin(x)/end(x)exist in an associated namespace.
// type definition
struct MyRange {
int* data;
size_t n;
int* begin() { return data; }
int* end() { return data + n; }
};
MyRange r = ...;
for (int x : r) { /* ....*/ }
Non-member example:
struct Buffer;
const int* begin(const Buffer& b);
const int* end(const Buffer& b);
Const correctness: for const objects you need begin / end overloads that work on const.
Practical patterns
1. When you need an index
Use C++20 std::ranges::views::enumerate, a classic index for, or a separate counter.
size_t i = 0;
for (const auto& x : vec) {
use(i, x);
++i;
}
2. initializer_list and temporaries
for (int x : {1, 2, 3}) { }
3. Reverse iteration
Range-based for is not reverse. If rbegin / rend exist:
for (auto it = vec.rbegin(); it != vec.rend(); ++it) { }
// or C++20 ranges reverse_view
4. const containers and intent to mutate
void print(const std::vector<int>& v) {
for (int x : v) { } // copy
for (const auto& x : v) { } // preferred for read-only
}
5. Readability: long range expressions
for (const auto& item : obj.get_container().get_items()) {
// get_container() is not called each iteration (range is evaluated once)
}
Per the standard, the range expression is evaluated once.
Relation to C++20 std::ranges
C++20 std::ranges composes naturally with range-based for when you use views (lazy sequences).
#include <ranges>
// example
std::vector<int> v = {1, 2, 3, 4, 5};
for (int x : v | std::views::filter([](int n) { return n % 2 == 0; })) {
std::cout << x << ' ';
}
Here the entire piped expression is the “range”; views are usually cheap to pass by value. If the range expression’s type models the ranges concepts, begin / end resolution follows the extended rules (see the ranges reference when your project is on C++20).
vector<bool> in depth (proxy)
std::vector<bool> is a packed-bit specialization: operator[] may return a proxy, not a real reference to bool. Generic code that assumes std::vector<T>::reference is T& can fail for T == bool; in generic code consider treating vector<bool> specially or using std::deque<bool> / std::vector<char>. Everyday use of range-based for with auto& x to traverse and assign usually works.
Summary
| Topic | Takeaway |
|---|---|
auto | Copy of element; original unchanged |
auto& / const auto& | Alias; mutate vs read-only |
| Temporaries | Range temporary lifetime extended for the loop; do not leak references out |
| Structured bindings | Handy for maps, pairs, tuples |
| Custom types | begin/end or member begin/end |