Skip to content
VLSI Mentor

C++ Part I · Module 5 · Control Flow

Range-Based for Loops

Every loop in the previous chapter built a number so that something else could be reached with it. The index was never the point — it was machinery, and it was machinery with two ends that both had to be right. This chapter removes it. A range-based for loop hands you each element in turn, which eliminates the entire bounds problem at once: there is no first index to get wrong, no last index to overshoot, and nothing to compare against a length. That is why it is the loop you will write most often in real C++, and why this module teaches it now rather than waiting for the containers it is usually demonstrated on. In exchange it introduces one decision the counting loop never had to make. When the loop gives you the element itself, you have to say what you want: a copy you can scribble on, the element itself so you can change it, or a read-only view that says you will not. All three are one character apart in the source, all three compile, and choosing wrong does not fail — it quietly works on a copy and discards everything you did. The compiler catches that in exactly one narrow case, which the chapter measures rather than assumes.

Foundation17 min readC++Part IFundamentalsControl Flow

Part I · Module 5 · Chapter 5.6 · Range-Based for Loops

1. The Loop Without an Index

Chapter 5.5's loops all had the same shape: build a number, use the number to reach something.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int i = 0; i < n; ++i) use(items[i]);

Three things there can be wrong — where i starts, what it stops before, and whether items[i] is in range — and only the first two are visible in the header.

A range-based for loop removes the number:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int width : widths) use(width);

Read it as: for each width in widths. There is no index, so there is no start, no bound and no comparison. The loop visits every element exactly once and then stops, and none of that is arithmetic you did.

The counting loop asks "which position?" The range-based loop asks "which element?" Most code only ever wanted the second question answered.

2. Something to Iterate

The reader at this point in the curriculum has met types, operators and control flow, and has not yet met a collection. Arrays are Chapter 9.1's subject and the standard containers are Module 11's. So this chapter borrows exactly two things and nothing more.

A braced list needs no type at all and works today:

Azvya Education Pvt. Ltd.VLSI Mentor
ranges.cpp — complete program
#include <iostream>

int main()
{
    // a braced list needs no container type at all
    for (int width : {8, 16, 32, 64})
        std::cout << width << "-bit ";
    std::cout << '\n';

    // a built-in array: Chapter 9.1's subject, borrowed here for somewhere to iterate
    int delays[] = {3, 7, 2, 9};
    int total = 0;
    for (int d : delays) total += d;
    std::cout << "total = " << total << "   elements = " << sizeof delays / sizeof delays[0] << '\n';
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
8-bit 16-bit 32-bit 64-bit
total = 21   elements = 4

int delays[] = {3, 7, 2, 9}; declares four ints stored one after another. That is the whole of what this chapter needs from Chapter 9.1 — the array's layout, its decay to a pointer when passed to a function, and the fact that C++ does not check your indices are all that chapter's business. Chapter 4.1 borrowed if the same way, and the same rule applies: use it, do not build on it.

Notice what the loop did not need. No 4, no sizeof, no bound at all. The expression sizeof delays / sizeof delays[0] appears once, in the output line, purely to show the count is 4 — and the loop above it never used that number.

3. What the Loop Is Doing

A range-based for loop is defined in terms of a plain loop. The standard describes it as an expansion, and that expansion is something you can write out:

Azvya Education Pvt. Ltd.VLSI Mentor
expansion.cpp — complete program. Both loops do the same work.
#include <iostream>

int main()
{
    int a[] = {10, 20, 30};

    // the hand-written equivalent
    for (auto it = std::begin(a), e = std::end(a); it != e; ++it)
        std::cout << ' ' << *it;
    std::cout << '\n';

    for (int v : a) std::cout << ' ' << v;
    std::cout << '\n';
    return 0;
}

Expected output — identical lines:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 10 20 30
 10 20 30

Two things follow from that, and they are the reason this section exists.

"Anything with a begin and an end" is literal. The loop works on whatever supplies those two things. A built-in array does, a braced list does, and every standard container will. That is why one loop form covers all of them, and why Module 11 will not need to teach you a new loop.

The range expression is evaluated once. std::begin(a) and std::end(a) are taken before the first iteration, not re-checked each time. For the arrays here that is simply efficient. It becomes a correctness rule once a collection can change size, which is Module 11's subject.

4. The Loop Variable Is a New Object Each Time

Here is the part that matters. In this form:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int d : delays)

d is a new int, initialized from the element. It is a copy — the same copy Chapter 6.2 will establish for function arguments, arriving one module early because this is where it first bites.

So writing to d writes to the copy:

Azvya Education Pvt. Ltd.VLSI Mentor
byvalue.cpp — complete program. The array is not modified.
#include <iostream>

static void show(const char* label, const int (&a)[4])
{
    std::cout << label;
    for (int v : a) std::cout << ' ' << v;
    std::cout << '\n';
}

int main()
{
    int delays[] = {3, 7, 2, 9};

    for (int d : delays) d *= 2;            // copy: the array is untouched
    show("after by value     :", delays);

    for (int& d : delays) d *= 2;           // reference: the array changes
    show("after by reference :", delays);
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
after by value     : 3 7 2 9
after by reference : 6 14 4 18

The first loop did nothing. It ran four times, doubled four copies, and threw all four away. The array is exactly as it started.

The second loop worked, and the difference in the source is one character: int& instead of int.

This particular program does get a warning, because its first loop writes d and never reads it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
byvalue.cpp:14:14: warning: variable 'd' set but not used [-Wunused-but-set-variable]

Do not read that as a safety net. §6 measures what happens the moment the body also reads the copy, which is what real code does.

5. The Three Forms

Formd isCan modify the element?Use when
for (int d : a)a copynoyou only read, and the type is cheap to copy
for (int& d : a)the elementyesyou need to change the collection
for (const int& d : a)a read-only viewno, enforcedyou only read, and copying is not free

The middle form is worth saying precisely. int& d makes d another name for the element — Chapter 6.4's framing, and Chapter 8.3's full treatment. Assigning to d assigns to the element, because there is no second object anywhere.

The third form combines both ideas. const is Chapter 3.9's, and here it is doing a job it could not do on a copy: the compiler refuses the write.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (const int& v : a) { v *= 2; }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
error: cannot assign to variable 'v' with const-qualified type 'const int &'

That is an error, not a warning. The read-only intent is checked rather than trusted — which is the whole argument for writing it down.

6. How Much Help the Compiler Gives

This is where it would be easy to overstate things, so here is exactly what was measured.

Write the by-value mistake with a body that only writes the copy, and the compiler does notice:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (int d : delays) d *= 2;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
warning: variable 'd' set but not used [-Wunused-but-set-variable]

Now add a read of d in the body — printing it, say, which is a thoroughly ordinary thing to do:

Azvya Education Pvt. Ltd.VLSI Mentor
noWarning.cpp — complete program. Zero warnings. Still wrong.
#include <iostream>

int main()
{
    int delays[] = {3, 7, 2, 9};
    for (int d : delays) {
        d *= 2;                       // modifies the copy
        std::cout << ' ' << d;        // ...and reads it, so it is "used"
    }
    std::cout << '\n';
    for (int v : delays) std::cout << ' ' << v;   // the array, unchanged
    std::cout << '\n';
    return 0;
}

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
 6 14 4 18
 3 7 2 9
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ c++ -std=c++17 -Wall -Wextra -Wpedantic noWarning.cpp -o noWarning
$                                            ← zero warnings

The first line shows the doubled values — the program clearly computed them. The second line shows the array untouched. And there is no diagnostic at all.

The warning only fires when the copy is written and never read. That is a narrow case, and it is not the one that appears in real code, because real code usually does something with the value it just computed. The compiler is reporting an unused variable, which it happens to be; it is not reporting "you meant to modify the array", which it cannot know.

7. Why const auto& Is the Default — and What Cannot Be Shown Yet

The form in this chapter's index entry is this one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    for (const auto& item : items) { /* ... */ }

Two parts, and auto is Chapter 3.10's: it deduces the element type so a change to the collection does not require editing the loop.

The const& part has two justifications, and only one of them can be demonstrated here.

The justification this chapter can prove: it states intent and the compiler enforces it. §5 measured the error. A reader of const auto& knows the loop does not modify anything, and knows it because the compiler checked.

The justification this chapter cannot prove: avoiding the copy. Every type available at this point in the curriculum is a fundamental type from Module 3 — an int, a double, a char — and copying one of those costs nothing worth measuring. A benchmark here would show no difference, and presenting one would be dishonest.

The cost becomes real when the elements are types whose copies do work: allocate memory, copy characters, acquire something. Chapter 6.5 is where that argument is made properly, for function parameters, and Chapter 9.5's std::string is the first type where you can watch it happen. By Module 11 a by-value loop over a container of strings is a genuine performance defect rather than a stylistic one.

So the honest summary is: write const auto& now because it says what you mean and the compiler checks it, and keep writing it later for a second reason that has not arrived yet. The habit is right for a reason you can verify today, which is the only basis this curriculum will ask you to form one on.

8. What This Chapter Does Not Cover

Two hazards exist that belong to later chapters, and naming them is better than letting you meet them unprepared.

Modifying the collection while looping over it. §3 established that the begin and end are taken once, before the first iteration. For a collection that can grow or shrink, adding or removing elements during the loop can leave those saved positions referring to storage that has moved. Built-in arrays cannot change size, so the problem cannot arise in this chapter. Module 11 owns it.

Looping over something temporary. If the range expression produces a value that exists only for the duration of that expression, the loop may end up traversing something that no longer exists. Diagnosing that requires the object-lifetime rules of Module 7 and the dangling-reference material of Chapter 8.8. It is named here so that for (auto x : make_things()) looks like a question rather than a convenience.

Neither is a reason to avoid the loop. Both are reasons to know which chapter to return to.

9. What to Actually Do

Reach for the range-based form first. If you are visiting every element, it is the correct loop and it has no bounds to get wrong.

Write const auto& to read. It is the default, it states intent, and the compiler enforces it.

Write auto& to modify. One character, and it is the difference between working and silently not.

Take a plain copy deliberately, when you specifically want a scratch value you can change without touching the collection.

Use Chapter 5.5's counting loop when you genuinely need the position, a step other than one, or to walk backwards.

Do not add or remove elements during the loop — Module 11.

10. Mistakes Worth Making Once

Omitting the & when you meant to modify. Measured: the array stayed 3 7 2 9 while the body computed 6 14 4 18.

Expecting a warning for that. Measured: zero, as soon as the body reads the copy.

Thinking -Wunused-but-set-variable is a safety net. It fires only when the copy is written and never read.

Writing through a const auto&. A compile error, which is the point.

Using the range-based form when you need the index. It does not give you one; Chapter 5.5's loop does.

Assuming the bound is re-checked each iteration. Begin and end are taken once.

Copying by value out of habit on expensive types. Free here, not free from Chapter 9.5 onward.

Iterating over something temporary. A lifetime question that Module 8 answers.

11. Interview Relevance

This is the module's Critical chapter and the questions are predictable.

"What is the difference between for (auto x : c) and for (auto& x : c)?" The first copies each element; the second names it. Writing to the first changes nothing in the collection. Say that, and if you can add that the compiler generally does not warn — because both forms are legitimate code — you have answered the question behind the question.

"Why const auto&?" Two reasons: it prevents accidental modification and the compiler enforces that, and it avoids a copy that costs something for non-trivial types. Giving both, and being clear which applies to an int, is a better answer than reciting the idiom.

"When would you not use a range-based for loop?" When you need the index, a step other than one, to iterate backwards, or to iterate over two things together. Knowing the limits is what shows you chose it rather than defaulted to it.

12. What You Should Be Able to Say Now

  1. A range-based for loop visits each element and removes the index, so both ends of Chapter 5.5's bounds problem disappear.
  2. It works on anything with a begin and an end — a braced list, a built-in array, and every standard container.
  3. It is defined as an expansion to a plain loop over std::begin and std::end; the hand-written version prints the same thing.
  4. Begin and end are taken once, before the first iteration.
  5. for (int d : a) makes d a copy of the element, new on every iteration.
  6. Measured: for (int d : delays) d *= 2; left the array as 3 7 2 9.
  7. Measured: for (int& d : delays) d *= 2; changed it to 6 14 4 18. One character.
  8. int& d makes d another name for the element — there is no second object.
  9. const int& d makes the read-only intent a compile error to violate: cannot assign to variable 'v' with const-qualified type.
  10. Measured: the by-value mistake warns only when the copy is written and never read; add a read and there are zero warnings and the array is still unchanged.
  11. That is why this chapter is Critical: both forms are correct code, so no diagnostic can exist for picking the wrong one — the same reason Chapter 5.3's break is silent.
  12. const auto& is the default because it states intent and is enforced; the copy-avoidance reason is real and cannot be demonstrated until Chapter 9.5.
  13. Two hazards belong elsewhere: changing the collection mid-loop (Module 11) and iterating a temporary (Chapter 8.8).

13. Exercises

Exercise 1 — predict the array. For int a[] = {1, 2, 3}; give the contents of a after each: (a) for (int v : a) v += 10; (b) for (int& v : a) v += 10; (c) for (const int& v : a) std::cout << v;.

Exercise 2 — pick the form. Choose auto, auto& or const auto& and justify in one clause: (a) print every element (b) double every element (c) sum the elements (d) clamp every element to a maximum (e) count how many are zero.

Exercise 3 — explain the silence. §6's program printed 6 14 4 18 and then 3 7 2 9, with zero warnings. Explain what each line shows, and why no diagnostic is available.

Exercise 4 — make it an error. Rewrite (a) from Exercise 1 so the mistake becomes a compile error instead of a silent no-op, and give the error text.

Exercise 5 — the narrow warning. State the one case where -Wunused-but-set-variable catches a missing &, and the smallest change to the body that makes it stop firing.

Exercise 6 — which loop? Choose the range-based form or Chapter 5.5's counting form, with a reason: (a) print every delay (b) print every second delay (c) print the delays in reverse (d) report the position of the largest delay (e) add 1 to every delay.

Exercise 7 — the honest default. A colleague says const auto& is pointless for an array of int because the copy is free. Say what is right about that, what is still true for int, and at which chapter the other half of the argument starts to apply.

14. Summary

  • A range-based for loop visits elements, not positions — and the entire bounds problem goes with the index.
  • It works on anything with a begin and an end, and is defined as an expansion over std::begin / std::end, taken once.
  • for (int d : a) gives a copy; for (int& d : a) gives the element; for (const int& d : a) gives an enforced read-only view.
  • Measured: by value left 3 7 2 9; by reference gave 6 14 4 18.
  • Writing through a const& is a compile error, which is why the intent is worth writing down.
  • Measured: the by-value mistake warns only when the copy is written and never read — add a read and there are zero warnings and the collection is still unchanged.
  • Both forms are correct code, so no diagnostic can distinguish them. That is what makes this chapter Critical.
  • const auto& is the default for a reason you can verify now; the copy-cost reason arrives at Chapter 9.5.
  • Use a counting loop for a position, a step, or reverse order.
  • Changing the collection mid-loop is Module 11; iterating a temporary is Chapter 8.8.

One subject is left in this module. Every loop so far ran its body from top to bottom and tested its condition at the appointed moment, and real loops do not always want that — sometimes the answer is found halfway through and there is no reason to continue, and sometimes one iteration should be abandoned without ending the loop. C++ has a statement for each. Both are useful, both are easy to use correctly, and one of them interacts with Chapter 5.4's progress guarantee in a way that produces a loop which runs forever while looking entirely reasonable. The last chapter measures that, along with two mistakes that account for most of the loop bugs beginners write — and finds that the compiler's willingness to warn about them depends on something as arbitrary as where you put a semicolon.

Standards & specifications

Governing standard
ISO/IEC 14882 (the C++ language standard)(opens ISO/IEC (JTC 1/SC 22/WG 21) in a new tab)

Defines the C++ language and its standard library — the type system and templates, the object model and object lifetime, initialisation and overload resolution, the memory model and the containers, algorithms and utilities the library provides. Like C's standard it is a FAMILY of editions (C++98/03, C++11, C++14, C++17, C++20, C++23) and the editions are NOT interchangeable: a feature this curriculum relies on may be unavailable, differently specified or merely proposed in another edition, so each lesson names the edition its claim depends on. It does not define any foreign-function interface to a hardware description language, nor what a compiler actually emits, nor platform behaviour on real silicon — those are the implementation's and the device's contracts. Working drafts published by WG21 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C++ Programming curriculum.