SovranCode
Modern C++ Lesson 19
Course contents50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
Modern C++50 available · 0 planned

Get a program running

01 · C++ Introduction: What Is C++?02 · How to Install a C++ Compiler and Set Up Your Editor03 · C++ Hello World: Your First Program Explained04 · How to Compile and Run C++ Programs05 · C++ Debugging: Warnings and Breakpoints

Work with values and input

06 · C++ Variables: Initialization and Scope07 · C++ Data Types: int, double and Type Conversion08 · C++ const, constexpr and auto Explained09 · C++ Operators: Arithmetic, Comparison and Logic10 · C++ User Input: cin, getline and Validation

Control flow and functions

11 · C++ Conditions: if, else if and else12 · C++ Switch Statement: Cases, break and default13 · C++ For Loop: Syntax and Examples14 · C++ While and Do-While Loops: Examples15 · C++ Functions: Parameters and Return Values

Strings and sequences

16 · C++ Strings: std::string with Examples17 · C++ Vector: std::vector with Examples18 · C++ Arrays: std::array and Bounds Checking19 · C++ Range-Based For Loop and Iterators20 · C++ Vector Practice: Build a Data Summary

Model data with classes

21 · C++ Structs and enum class Explained22 · C++ Classes and Objects: Encapsulation23 · C++ Constructors and Member Initialization24 · C++ Const Member Functions and Composition25 · C++ Copying Objects and the Rule of Zero

Lifetime and resource ownership

26 · C++ Object Lifetime, Destructors and RAII27 · C++ Pointers and References: nullptr and Borrowing28 · C++ unique_ptr: Smart Pointers and Ownership29 · C++ Move Semantics: std::move vs Copying30 · C++ string_view and span: Avoid Dangling Views

Containers and algorithms

31 · C++ map vs unordered_map: Lookup and Examples32 · C++ set, queue and stack: When to Use Each33 · C++ Lambda Expressions and STL Algorithms34 · C++ sort and binary_search: Rules and Complexity35 · C++20 Ranges: Views, filter and transform

Errors, files, and interfaces

36 · C++ optional and variant Explained37 · C++ Exceptions: try, catch and Safe Cleanup38 · C++ File Handling: Read and Write Text Files39 · C++ Input Parsing and Data Validation40 · C++ Polymorphism: Virtual Functions and Interfaces

Generic programming and quality

41 · C++ Templates: Function and Class Examples42 · C++20 Concepts and Template Constraints43 · C++ Unit Testing: Normal and Boundary Cases44 · C++ Sanitizers: Find Memory and Undefined Behavior Bugs45 · C++ Performance: Benchmark Before Optimizing

Build and deliver a complete application

46 · C++ Header Files and Translation Units47 · C++ CMake Tutorial: Build an Executable48 · C++ Dependencies and CTest: Automated Tests49 · C++ Multithreading: Threads and Data Races50 · C++ Command-Line Applications: Help and Release Checks
PREVIOUS LESSONC++ Arrays: std::array and Bounds Checking
NEXT LESSONC++ Vector Practice: Build a Data Summary
C++ foundations · Lesson 19 About 42 min with practice

C++ Range-Based For Loop and Iterators

Learn C++ range-based for loops, copies versus references, begin/end iterators, vector invalidation and safe erase-while-iterating patterns.

Before you begin

Understand counter-based loops, functions, std::string, vector and array. Know that structural vector changes can invalidate borrowed access.

What you will learn

  • Choose value, mutable reference or const reference loop variables.
  • Explain begin/end as a half-open traversal boundary.
  • Avoid vector growth and erasure inside a range-based traversal.
  • Use erase's returned iterator without skipping adjacent matches.
  • Test filtering for empty, all-kept, all-removed and adjacent-removal inputs.

Range-based for expresses visiting every element

A counter-based loop is useful when position is part of the calculation. If the task is simply to visit each value, a range-based for loop states that intention directly: for (int score : scores). The loop obtains each existing element in order, so you do not manually maintain an index or repeat size comparisons. This removes one source of off-by-one mistakes without deciding whether you are reading a copy or the original element.

The declaration before the colon matters. An ordinary value variable receives a copy for these integer and string examples. int& refers to an existing integer and allows changes through that name; const auto& observes an element without copying it or permitting modification through that reference. Choose based on the operation you intend. Copying a small integer is normal, while copying every large string just to print it is often unnecessary.

Do not treat auto as an automatic reference qualifier. for (auto label : labels) still creates copies of the strings in this lesson. Mutating label does not update the stored string. Adding an ampersand changes that contract, not just performance. When reviewing a loop, explain both its traversal boundary and the ownership or borrowing behavior of its loop variable before discussing the statements inside the body.

An iterator points at the first of two adjacent negative values. Erase returns the next surviving position to inspect, which can still be negative. Only the keep branch increments the iterator; the end sentinel is compared but never dereferenced.
After erase, use the returned position. Increment only when keeping an element.
C++ PRACTICE RUNNER

Compare a copied loop variable with a mutable reference

The first loop changes temporary strings; the second changes the vector's owned strings. Read-only printing uses const references.

#include <iostream>
#include <string>
#include <vector>
int main() {
    std::vector<std::string> labels{"a", "bb"};
    for (auto label : labels) label += '!';
    std::cout << "After copies: " << labels.at(0) << '\n';
    for (auto& label : labels) label += '!';
    for (const auto& label : labels) std::cout << label << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
After copies: a
a!
bb!

After editing inputs, calculate a new prediction; this reference describes only the starter.

begin points to a candidate; end is a boundary

An iterator represents a position in a sequence and supports the operations required by its category. For this vector traversal, begin() provides the first position and end() provides the past-the-end boundary. Compare the positions before dereferencing with *. When begin equals end, the sequence is empty and the loop body must not execute. The boundary is useful for comparisons but is not a hidden extra element you may read.

An explicit iterator loop can therefore test it != values.end(), inspect *it, then increment it. Read that as a half-open interval, the same model used for valid indices. Do not assume all iterators support arbitrary addition, subtraction or integer comparison just because vector iterators permit more than simple increment. Later container choices change available operations and costs. Write only the operations the current algorithm actually requires.

A range-based loop conceptually keeps traversal positions of its own. You do not get those positions back merely by declaring the loop variable as a reference. A reference changes how the current element is accessed; it does not redesign the loop's cached boundary. This explains why a concise range loop is excellent for observing elements but is not automatically the right tool for structural edits to the collection being traversed.

Changing an element differs from changing the sequence

Assigning a new integer to an existing element preserves the vector's shape. Appending, inserting and erasing can change positions or storage and therefore affect traversal validity. Reallocation invalidates all element references, pointers and iterators. Erasure invalidates positions at or after the erased location. Keep these rules connected to the particular operation instead of saying every change is always safe or every change is always forbidden.

Do not push_back into the same vector from its range-based loop, even if you reserved enough capacity first. A successful append changes the past-the-end position; avoiding reallocation does not preserve that old boundary. For a transformation that produces extra values, collect them in a separate output vector, finish traversal, and then decide how to combine results. Separating source and destination also makes the intended result easier to specify and test.

Do not erase through a saved iterator and then increment that invalidated iterator. A range variable cannot repair the hidden traversal state either. Instead choose an algorithm whose mutation contract gives you a valid next position, or construct a separate filtered collection. C++20 also offers std::erase_if for supported containers, but the later algorithms lesson will introduce predicates. Here the explicit iterator approach keeps each state change visible.

C++ PRACTICE RUNNER

Erase adjacent matches without skipping the next value

The returned iterator may point at another even number. The erase branch does not increment it again.

#include <iostream>
#include <vector>
int main() {
    std::vector<int> values{2, 4, 5, 6, 7};
    for (auto it = values.begin(); it != values.end();) {
        if (*it % 2 == 0) it = values.erase(it);
        else ++it;
    }
    for (int value : values) std::cout << value << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Compare with the starter's expected output
5
7

After editing inputs, calculate a new prediction; this reference describes only the starter.

Explain the erase loop before adapting it

At the beginning of each iteration, it is either a valid position to inspect or the current end boundary. The condition establishes which case applies. If the value must be removed, erase returns the position following the removed element in the updated sequence, possibly end. Assign that returned value to it. If the element is kept, advance once so it will not be inspected forever. Every branch makes progress without using an invalidated position.

Removing the first of two adjacent matches shifts the next match into the position that now needs inspection. Incrementing again would skip it. Removing the last element returns end, which the next condition recognizes before dereferencing. These are not exotic edge cases: they are the exact cases that distinguish a valid erase loop from one that happens to work when matches are spaced apart. Trace both with a short handwritten sequence.

Repeated vector erasure can move many elements, so this teaching pattern is not automatically the fastest way to filter a large collection. Its purpose here is correctness and explicit invalidation handling. An algorithm that compacts survivors in one pass can be preferable later. The lab bounds the input to six values, making the simple approach reasonable without teaching that its cost scales efficiently to an unlimited data set.

Validate first, mutate second, report third

The lab reads a count from zero through six, followed by that many integer lines from minus one hundred through one hundred. Malformed input, overflow, missing required lines and out-of-range values all produce Invalid input and status one. Collect the entire accepted sequence before filtering. That keeps input failure separate from the algorithm and prevents a partial success report from appearing after only some records were valid.

Remove every negative value in place, preserve the relative order of nonnegative values, then print the retained count and one Value line for each survivor. Zero is a retained value, not an empty marker. An initially empty sequence and a sequence whose elements are all removed both print Kept: 0 without reading front or dereferencing begin. Extra lines beyond the requested record count are outside this single-report protocol and remain unread.

Test consecutive negatives at the beginning, middle and end, all-kept values, all-removed values, zero, a final input line without a newline and count zero. Compare every output line, not just the count, because an algorithm can preserve the right number of elements but reorder them incorrectly. Finally, explain which branch advances the iterator and why no invalidated iterator is reused. That explanation is stronger evidence than one successful run.

LOCAL TERMINAL COMMANDS

Compile and test outside the lesson

The compiler-backed editors support complete single-file programs when the execution service is available. Otherwise save one program as main.cpp and run these commands with your configured compiler. Supply all standard input before Run; this is not an interactive terminal.

macOS / Linux (choose your installed compiler):
clang++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
# Or: g++ -std=c++20 -Wall -Wextra -Wpedantic main.cpp -o lesson
./lesson

Windows Developer Command Prompt:
cl /std:c++20 /EHsc /W4 main.cpp /Fe:lesson.exe
lesson.exe
APPLY THE CONCEPT

Independent lab

Collect a bounded integer sequence, remove negative values safely with iterators, and preserve survivor order.

Acceptance checklist

  • Read a whole-line count in 0 through 6 and then that many whole-line values in -100 through 100.
  • Reject bad or missing required input with Invalid input and status 1 before reporting.
  • Use erase's returned iterator when removing a value; increment only on the keep branch.
  • Retain zero and preserve the order of all nonnegative values.
  • Print Kept followed by one Value line per survivor, including an explicit zero-count result.
Need a hint?
  1. Check it against the current end before using *it.
  2. Adjacent negatives expose an extra increment after erase.
  3. Use a range-based value loop for final read-only integer output.
C++ PRACTICE RUNNER

Write and run your lab

Build your own solution before revealing the reference. Use the input field for normal, boundary and invalid cases; this runner executes your program but does not grade it.

#include <iostream>
#include <string>
#include <sstream>
#include <limits>

int main() {
    // Implement the acceptance checklist above.
    return 0;
}

TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Your lab evidence and notes

Record predictions, actual output, commands and the cause of each repair. Code is plain text: this scratchpad does not execute or grade C++.

Loading local notes…

Review your acceptance checklist and quiz explanations before marking this lesson complete. This is self-review, not a grade or certification.

Reveal the explained lab solution
C++ PRACTICE RUNNER

Reference lab solution

Compare the reference with your own reasoning. Running this solution does not grade or complete the lesson.

#include <iostream>
#include <sstream>
#include <string>
bool readInteger(int& value) {
    std::string line;
    if (!std::getline(std::cin, line)) return false;
    std::istringstream parser{line};
    if (!(parser >> value)) return false;
    parser >> std::ws;
    return parser.eof();
}
#include <vector>
int main() {
    int count{};
    if (!readInteger(count) || count < 0 || count > 6) {
        std::cout << "Invalid input\n"; return 1;
    }
    std::vector<int> values;
    for (int i = 0; i < count; ++i) {
        int value{};
        if (!readInteger(value) || value < -100 || value > 100) {
            std::cout << "Invalid input\n"; return 1;
        }
        values.push_back(value);
    }
    for (auto it = values.begin(); it != values.end();) {
        if (*it < 0) it = values.erase(it);
        else ++it;
    }
    std::cout << "Kept: " << values.size() << '\n';
    for (int value : values) std::cout << "Value: " << value << '\n';
}
TERMINAL OUTPUT
Predict the output, edit the source, then select Run code.

C++20 compiler runner: source and input are sent only when you press Run to this website's execution endpoint, not an external vendor. Local preview requires Docker and gcc:14; production requires a separately isolated execution service. Network is disabled; execution is limited to 3 seconds, compilation to 10 seconds, memory to 384 MB and output to 32 KB. Single-file programs only. Don't enter secrets. Edits reset on refresh; Copy your work to keep it.

Expected output

Kept: 3
Value: 1
Value: 0
Value: 2

Collection finishes before mutation, so a parse error cannot leave a misleading partial report.

After erasing, the next value may have shifted into the same location; assigning erase's return avoids both stale positions and skipped neighbors.

The final traversal performs no structural edits, so a range-based loop clearly expresses reporting the survivors in their original order.

Review before moving on

Explain the lesson's objectives in your own words, compare your lab evidence with the acceptance checklist, and keep the first failing case alongside your repair. Completing a page is not proof that every C++ feature or input is supported.

  • Choose value, mutable reference or const reference loop variables.
  • Explain begin/end as a half-open traversal boundary.
  • Avoid vector growth and erasure inside a range-based traversal.
  • Use erase's returned iterator without skipping adjacent matches.
  • Test filtering for empty, all-kept, all-removed and adjacent-removal inputs.

References and further reading

Original examples and exercises accompany these primary references. Tool-specific diagnostics and support depend on your installed version.

  • C++ draft: range-based for statement
  • C++ draft: vector erasure and invalidation
KNOWLEDGE CHECK

Check your understanding

Answer all five questions, then review the explanations. Answers are not saved or synced; completion is a separate self-review choice.

01Does for (auto value : integerVector) modify stored integers when value changes?
02May you dereference end()?
03What should you use after vector::erase(it)?
04Does reserve make push_back inside the same range-for safe?
05Which test catches an extra increment after erase?
PREVIOUS LESSONC++ Arrays: std::array and Bounds Checking
NEXT LESSONC++ Vector Practice: Build a Data Summary
ON THIS PAGELearning objectivesRange-based for expresses visiting every elementbegin points to a candidate; end is a boundaryChanging an element differs from changing the sequenceExplain the erase loop before adapting itValidate first, mutate second, report thirdIndependent labReviewKnowledge checkReferences
Course overview