AlgoMaster Logo

Range-Based For Loop

Medium Priority17 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

Walking through every item in a container is one of the most common operations any program performs: print every product in a catalog, sum the prices in a cart, mark every order as shipped. The classic for loop works for this, but it is noisy because half the line manages an index variable that the body never uses directly. The range-based for loop, added in C++11, removes that noise. Name the container, name each element, and the loop handles the rest.

The Basic Syntax

A range-based for loop has this shape:

declaration is the name (and type) for each element inside the body. range is the container being iterated over. On each pass, the loop pulls the next element out of the container and binds it to the declared name.

A small, useful version sums the prices in a shopping cart:

Read it almost like an English sentence: "for each price in cartPrices, add it to the total." No index, no size() call, no risk of writing <= size() instead of < size() and walking off the end.

The loop runs once for every element, in the order the container stores them. For a std::vector, that is first to last. When the loop runs out of elements, control falls through to the line after the closing brace.

What Can Go on the Right Side

The expression to the right of the colon does not have to be a std::vector. The range-based for loop works on anything that exposes its elements through a pair of begin() and end() functions. That covers nearly every standard container:

  • C-style arrays: int sizes[] = {7, 9, 11};
  • std::vector (resizable, the default choice for sequences)
  • std::array (fixed size, lives on the stack)
  • std::string (treated as a sequence of char)
  • std::map, std::unordered_map, std::set, std::list, std::deque
  • A braced-init list written directly in the loop, like {1, 2, 3}

A short example of each:

The last form is easy to overlook. A { ... } list can appear directly in the loop header, iterating over its elements without a named container. It is useful for short, fixed sets of values, such as trying a handful of test cases.

C-style arrays work in a range-based for only when their size is known at compile time. If the array has decayed to a pointer (for example, after being passed into a function as int arr[]), the loop has no element count to walk and the code will not compile. Use std::vector or std::array when passing containers around.

The Three Modes: Value, Reference, Const Reference

The most important choice when writing a range-based for loop is what appears on the left side of the colon. Three patterns are common, and they behave differently.

Mode 1: By Value (Copy Each Element)

On each pass, the loop copies the current element into the local variable price. That is fine and cheap when the elements are small built-in types like int, double, or char. It is not fine when the elements are heavy types like std::string, big classes, or vectors-of-things, because every iteration pays the cost of constructing and destroying a copy.

for (auto name : customerNames) where customerNames is a std::vector<std::string> copies every string on every iteration. For a list of ten thousand customers, that is ten thousand unnecessary allocations. Use const auto& name instead.

By-value is also the right choice when a local copy is needed that can be mutated without affecting the original container, but that case is rare.

Mode 2: By Reference (Read and Modify in Place)

The & turns the loop variable into a reference to the container's element. No copy is made. Whatever is assigned to price writes through to the actual element in the vector. This is the mode for changing every element, such as applying a discount, marking every order as shipped, or zeroing out a row of counters.

The full version:

The first loop modifies the vector in place. The second loop, which uses by-value, reads each element to print it. Two loops, two modes, each picked for its job.

Mode 3: By Const Reference (Read-Only, No Copy)

The const auto& form is a read-only reference. No copy is made (so it is cheap, even for std::string or large objects), and the element cannot be written to by accident (so the compiler catches bugs where a read was intended but a write was typed). For non-trivial element types, this is the safe default.

A simple rule: when the element is not being modified and is not a small built-in like int or double, use const auto& first. If later modification is needed, drop the const. If the type is tiny, switch to a plain auto copy.

Picking the Right Mode

The rough rule in table form:

GoalElement typeUse
Read, do not modifySmall (int, double, char, bool)for (auto x : items)
Read, do not modifyAnything else (string, class, vector)for (const auto& x : items)
Modify the elementAny typefor (auto& x : items)
Get a fresh local copy to mutateAny typefor (auto x : items)

The middle row is where the biggest performance wins hide. Forgetting & on a loop over a vector of strings is a common preventable performance bug in modern C++ code.

The flow above is the decision tree to run through when writing a range-based for.

Memory: Copy vs Reference, Side by Side

It helps to see what happens in memory when one mode is picked over another. Suppose cartPrices is {29.99, 14.99, 9.99} on the second pass of the loop.

The vector lives once in memory. When the loop uses auto, a fresh copy of slot 1 is made into price for that iteration, used, then destroyed at the end of the iteration. When the loop uses auto&, no copy is made; price is another name for the slot. Writing through that name writes the original element.

For an element type like double, the copy on the left side is one register's worth of data, so the difference is negligible. For an element type like std::string that owns heap memory, the copy version allocates a fresh buffer for every iteration. That is where the performance gap appears.

Iterating Over a std::map

Maps are slightly different. A std::map<Key, Value> stores pairs, and each "element" the loop sees is a std::pair<const Key, Value>. The old-style way to read those pairs was clunky:

entry.first is the key, entry.second is the value. It works, but the names are unhelpful. Since C++17, structured bindings let you name the two pieces directly:

The [product, price] syntax is a structured binding: it pulls the two fields of the pair into named local variables. The names can be anything; product and price happen to be descriptive here. Compile this with -std=c++17 or later.

Note the output order. std::map keeps its keys sorted, so the loop visits them in alphabetical order, not the order they were inserted. For insertion order, use std::unordered_map paired with a separate vector of keys, or a different container entirely. The STL section covers that trade-off in detail.

To modify the values (but not the keys, which are always const inside a map), drop the const:

The price reference writes back into the map. The product binding is still const because map keys cannot be mutated through iteration; mutating a key would corrupt the map's internal ordering.

The Init-Statement Form (C++20)

C++20 added one more variant. A single setup statement can run before the loop, in the loop header itself:

The first piece, auto items = getCart();, runs once before the loop starts. Its scope is the loop. The second piece is the regular range-based for. This is useful when the range comes from a function that returns by value, because it pins the result to a named variable inside the loop's scope.

A short example:

Without the init-statement form, auto items = getCart(); goes on one line and the for loop on the next. With it, both fit on a single line and items is scoped tightly to the loop. Compile this with -std=c++20.

This is a niche feature. Most range-based for loops will not need it. It exists for the case where a temporary should be kept alive only for the loop.

What You Lose: No Index

The range-based for loop hides the index. That is the point. But sometimes the index is needed, for example to print "Item 1: Mouse", "Item 2: Keyboard", and so on. Two clean ways recover it.

Option 1: A Manual Counter

Keep a separate integer that is incremented each iteration:

This works, but it is a small step backward toward the noise that range-based for was meant to remove. It is fine when the counter is incidental to the loop.

Option 2: Go Back to a Classic For

When the index is the point, the classic indexed for loop is clearer:

The rule of thumb: when only the elements matter, use range-based for. When positions matter (skipping every other one, accessing two elements at once, walking backward), use a classic for.

Don't Resize the Container Mid-Loop

One rule matters: do not change the size of the container while iterating over it. Adding elements with push_back may reallocate the vector's internal buffer, leaving the loop's invisible iterator pointing at freed memory. Erasing elements shifts what is behind them, so the loop skips or revisits items it should not.

This is called iterator invalidation, one of the classic C++ landmines. The fix depends on the operation:

  • To remove elements during iteration, use the erase-remove idiom or std::erase_if (C++20). The STL section covers both.
  • To add elements during iteration, finish the loop first and apply the changes after, or build a new container during the loop.
  • To modify existing elements in place, range-based for with auto& is fine; modification does not change the container's size, only its contents.

The short version: read-and-modify is safe. Resize-while-iterating is not.

A Larger Example: Processing Orders

The following program uses three different modes of range-based for in three places, on a single list of orders. It is a worked example of when to pick which mode.

Three loops, three modes. The first one reads. The second one mutates. The third one needs the amount field but does not need to modify the order, so a by-value copy works (and is slightly clearer than reaching into a reference). Each choice matches what the loop does.

The by-value loop in step 3 is not strictly cheap: it copies the whole Order struct including the customer's name. For a struct this small it is fine, but for a heavier type, const auto& order and then revenue += order.amount would be better. Picking the right mode is partly habit, partly thinking about the size of the element type.

When to Use Range-Based For

The range-based for loop is the default for any traversal. Before writing a classic indexed for loop, ask: is the index actually needed? If not, the range-based form is shorter and harder to get wrong.

A classic for is still the right choice in narrow cases:

  • The index itself is needed (numbered output, even/odd selection).
  • Iterating over two containers in lockstep (the parallel-index pattern).
  • Walking backward, skipping elements, or stepping by more than one.
  • Resizing the container during the walk.

Everything else is range-based for territory.

Quiz

Range-Based For Loop Quiz

10 quizzes