C++ Range-Based for: auto, References, Temporaries,
이 글의 핵심
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](/en/blog/cpp-perfect-forwarding/, 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](/en/blog/cpp-structured-binding/, 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 |
Related posts: [auto keyword](/en/blog/cpp-auto-keyword/, [structured bindings](/en/blog/cpp-structured-binding/, type deduction.
See also (internal links)
Related reading
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Range-based for in C++: auto vs auto& vs const auto&, proxy iterators, temporaries, C++17 structured bindings, custom.
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence. See the C++ series index for the full picture.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
Keywords Covered in This Article (Related Search Terms)
This article covers C++, range-for, for, C++11, C++17, iterator.