본문으로 건너뛰기 C++ Range-Based for: auto, References, Temporaries,

C++ Range-Based for: auto, References, Temporaries,

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

GoalSuggestion
Read-only, large typeconst auto&
Mutate elementsauto& (non-const range)
Cheap copy semanticsauto (small POD-like types)
Forwarding / generic signaturesauto&& (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, or
  • x.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

TopicTakeaway
autoCopy of element; original unchanged
auto& / const auto&Alias; mutate vs read-only
TemporariesRange temporary lifetime extended for the loop; do not leak references out
Structured bindingsHandy for maps, pairs, tuples
Custom typesbegin/end or member begin/end


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.


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.