C++ Range-Based for: auto, References, Temporaries, Structured Bindings — Practical Guide

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

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, 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, 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