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](/en/blog/cpp-loop-for-while-master/.
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](/en/blog/cpp-auto-type-deduction/.
See also (internal links)
- [C++
autokeyword](/en/blog/cpp-auto-keyword/ - [C++ structured bindings](/en/blog/cpp-structured-binding/
- [C++
autotype deduction](/en/blog/cpp-auto-type-deduction/
Related reading
- [C++ for and while — practical guide](/en/blog/cpp-loop-for-while-master/
- [Modern C++ (C++11–C++20) syntax cheat sheet](/en/blog/cpp-cheatsheet-02-modern-cpp-syntax/
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Range-based for in C++: auto vs auto& vs const auto&, proxy iterators, temporaries, C++17 structured bindings, custom be… 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- [C++ auto Type Deduction](/en/blog/cpp-auto-type-deduction/
- [C++ Custom Ranges](/en/blog/cpp-series-25-3-ranges-introduction/
- [C++ Structured Bindings : Tuples, Maps, and auto](/en/blog/cpp-structured-binding/
이 글에서 다루는 키워드 (관련 검색어)
C++, range-for, for, C++11, C++17, iterator 등으로 검색하시면 이 글이 도움이 됩니다.