Range-Based for Loop Errors in C++: Missing begin()/end() and Eight Other Pitfalls
Key takeaways
How a range-based for loop expands under the hood, and how that expansion explains nine common errors: missing or non-const begin()/end(), decayed arrays, vector<bool> proxies, silent copies of map pairs, invalidation, and the temporary that dies before the loop body runs.
Why range-for errors look strange
A range-based for loop is syntactic sugar. The errors are confusing because the compiler reports them in terms of the code it generated, not the code you wrote: you never typed begin, yet the message says 'begin' was not declared in this scope. Once you know the expansion, every error in this article becomes readable.
What the compiler actually writes
Since C++17, for (decl : expr) body is defined as roughly:
{
auto&& __range = expr;
auto __begin = /* begin-expr */;
auto __end = /* end-expr */;
for (; __begin != __end; ++__begin) {
decl = *__begin;
body
}
}
Three details matter:
- How begin-expr is chosen. For an array, it is the array pointer. For a class with a member named
beginorend, it is__range.begin()/__range.end(). Otherwise it is an unqualified callbegin(__range)/end(__range)found by argument-dependent lookup, which means a free function in the same namespace as your type. It does not automatically callstd::begin. __rangeisauto&&. It binds to whateverexpris, including a temporary, and extends that temporary’s life to the end of the loop. But only the final object, not temporaries used to compute it.__beginand__endare computed once. Anything that invalidates them during the loop is undefined behavior.
Error 1: ‘begin’ was not declared in this scope
struct Scores { int data[5] = {1, 2, 3, 4, 5}; };
Scores s;
for (auto x : s) { }
error: 'begin' was not declared in this scope; did you mean 'std::begin'?
error: 'end' was not declared in this scope; did you mean 'std::end'?
Scores has no members named begin/end, so the compiler tried ADL lookup and found nothing. The “did you mean ‘std::begin’?” suggestion is misleading: std::begin(s) would not work either, because it just calls s.begin(). Add the functions:
struct Scores {
int data[5] = {1, 2, 3, 4, 5};
int* begin() { return data; }
int* end() { return data + 5; }
const int* begin() const { return data; }
const int* end() const { return data + 5; }
};
If you cannot modify the type (it comes from a C library, say), define free begin/end functions in the same namespace as the type so ADL finds them. Defining them in your own namespace does not work.
Anything returned by begin() must support !=, prefix ++ and unary *. A raw pointer does, which is why returning data is enough.
Error 2: it works on objects but fails on const references
With only the first two (non-const) functions defined:
void print(const Scores& s) {
for (auto x : s) { }
}
error: passing 'const Scores' as 'this' argument discards qualifiers [-fpermissive]
__range is a const reference, so it can only call const member functions. Every custom range needs const overloads of begin() and end(), returning something that yields read-only elements. This is the same error as calling any non-const member on a const object; see the const errors article.
Error 3: arrays that are really pointers
void sum(int a[5]) {
for (int x : a) { } // error: 'begin' was not declared in this scope
}
int* p = new int[3];
for (int x : p) { } // same error
A function parameter declared as int a[5] is actually int* a; the 5 is ignored. A pointer carries no length, so the compiler has no idea where the range ends. Pass a std::array<int, 5>&, a std::vector<int>&, a reference to an array int (&a)[5], or, in C++20, a std::span<int>, which bundles a pointer with a size.
Error 4: the loop runs but changes nothing
std::vector<int> v{1, 2, 3};
for (auto x : v) x = 9;
std::cout << v[0]; // prints 1
decl = *__begin with auto x copies each element. GCC only hints at it with -Wall: warning: variable 'x' set but not used. Use auto& to modify, const auto& to read without copying. For int the copy is harmless; for a std::vector<std::string> it copies every string on every iteration.
Error 5: invalid initialization of reference with std::map
std::map<std::string, int> m;
for (std::pair<std::string, int>& kv : m) { }
error: invalid initialization of reference of type 'std::pair<std::__cxx11::basic_string<char>, int>&' from expression of type 'std::pair<const std::__cxx11::basic_string<char>, int>'
A map’s element type is std::pair<const Key, Value>: the key is const because changing it would break the tree ordering. The spelled-out type is wrong by one const. With a non-reference or const& declaration this compiles silently, and it is worse, because the compiler converts each element into a temporary copy of a different pair type. Use auto&, const auto&, or structured bindings:
for (auto& [key, value] : m) value += 1;
Error 6: vector refuses auto&
std::vector<bool> flags(3);
for (auto& b : flags) { }
error: cannot bind non-const lvalue reference of type 'std::_Bit_reference&' to an rvalue of type 'std::_Bit_iterator::reference'
std::vector<bool> packs bits, so there is no bool object to refer to. Dereferencing its iterator returns a proxy object by value, and auto& cannot bind to a temporary. for (auto&& b : flags) works, and assigning to b writes through the proxy into the vector. Generic code that must work with any container should use auto&& for this reason.
Error 7: modifying the container during the loop
std::vector<int> v{1, 2, 3};
for (int x : v) v.push_back(x * 2); // undefined behavior
This compiles. __begin and __end were computed before the first iteration; once push_back reallocates, both point into freed memory. Even without reallocation, __end is stale, so the loop never sees the new elements. The same applies to erase on vectors and to erase of the current element in maps.
Collect changes separately and apply them after the loop, or use an index loop that recomputes the size, or for removal use std::erase_if (C++20) or the erase-remove idiom. Details in iterator invalidation.
Error 8: the temporary that dies before the loop body
struct Doc {
std::vector<std::string> lines{"a", "b", "c"};
const std::vector<std::string>& get() const { return lines; }
};
Doc load();
for (auto& l : load().get()) std::cout << l.size(); // undefined behavior before C++23
Expand it: auto&& __range = load().get();. __range is bound to the vector reference returned by get(). The Doc temporary returned by load() is not what __range binds to, so it is destroyed at the end of that statement, taking lines with it. The loop then iterates freed memory. When I compiled this with GCC 10 at -O2, it printed a large garbage number instead of 111, with no warning even under -Wall -Wextra.
By contrast, for (auto& l : load().lines) is fine (member access on a temporary is extended), and so is for (auto& l : makeVector()). C++23 (P2718R0) extends the lifetime of all temporaries in the range expression, but until your compiler implements it, keep the owner alive explicitly:
auto doc = load();
for (auto& l : doc.get()) { }
Or use the C++20 init-statement: for (auto doc = load(); auto& l : doc.get()) { }.
Error 9: auto deduces a different type than you think
for (auto x : {1, 2.5}) fails with error: unable to deduce 'std::initializer_list<auto>&&' from '{1, 2.5e+0}', because the braced list has mixed types and no single std::initializer_list<T> can be deduced. for (auto x : {1, 2, 3}) works: it iterates an std::initializer_list<int>. If you need mixed types, spell out the list type.
Where this goes wrong in practice
The one I trust least is error 8, because it looks like the most idiomatic code in the article. A getter returning a const reference is good practice; a factory returning by value is good practice; chaining them in a range-for is a use-after-free. It typically passes tests because the freed memory still holds the old contents, and then breaks after an unrelated change to allocation patterns. When a loop iterates over something().something_else(), I now hoist the first call into a named variable without thinking about it.
The other is error 5 in its silent form. Code like for (const std::pair<std::string, int>& kv : m) compiles cleanly and looks efficient, but it copies every key string into a temporary pair on each iteration, because the spelled type is not the map’s element type. Profilers show it as unexpected string allocations in a loop that was supposed to be read-only. const auto& would have been both shorter and correct.
FAQ
Q. Why does for (auto& b : vec) fail when vec is a std::vector<bool>?
A. Its iterator returns a proxy object by value, not a bool&. Use auto&&, or switch to std::vector<char> if you need real references.
Q. Do I need to write a full iterator class to support range-for?
A. No. begin() must return something supporting !=, ++ and *, and end() something comparable to it with !=. A raw pointer satisfies that. Since C++17, end() may even return a different type (a sentinel) as long as begin() != end() compiles.