for loop
const variable
programming
coding tips
loop increment

How to make a for loop variable const with the exception of the increment statement?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

In C and C++, the loop variable in a for loop is mutable by design — it needs to be incremented each iteration. But you may want to prevent accidental modification of the loop variable inside the loop body while still allowing the increment in the for statement itself. Unfortunately, there is no direct language feature that makes a variable "const except for the increment." However, several patterns come close: range-based for loops, const references to the loop counter, and C++20 ranges.

The Problem

cpp
1for (int i = 0; i < 10; ++i) {
2    // Want to prevent this:
3    i = 5;      // Accidental reassignment — skips iterations
4    i += 2;     // Accidentally modifying the counter
5
6    // But allow this:
7    use(i);     // Reading is fine
8}

Making i const would prevent the increment in ++i, so for (const int i = 0; ...) does not compile.

Solution 1: Range-Based For with const (C++11)

The cleanest approach — iterate over a range with a const reference:

cpp
1#include <vector>
2#include <numeric>
3
4// Create a range of indices
5std::vector<int> range(10);
6std::iota(range.begin(), range.end(), 0);  // {0, 1, 2, ..., 9}
7
8for (const int i : range) {
9    // i = 5;  // Compile error — i is const
10    std::cout << i << " ";
11}

This works because the range-based for loop creates a new const int i each iteration, initialized from the next element.

Solution 2: C++20 std::views::iota

C++20 provides a lazy range that avoids allocating a vector:

cpp
1#include <ranges>
2#include <iostream>
3
4for (const int i : std::views::iota(0, 10)) {
5    // i = 5;  // Compile error — i is const
6    std::cout << i << " ";
7}
8// Output: 0 1 2 3 4 5 6 7 8 9
9
10// With step (every other number)
11for (const int i : std::views::iota(0, 10) | std::views::filter([](int x) { return x % 2 == 0; })) {
12    std::cout << i << " ";
13}
14// Output: 0 2 4 6 8

This is the most idiomatic modern C++ solution — no allocation, lazy evaluation, and the loop variable is truly const.

Solution 3: Lambda with Const Parameter

Wrap the loop body in a lambda that takes the index by const value:

cpp
1for (int i = 0; i < 10; ++i) {
2    [&](const int i) {
3        // i = 5;  // Compile error — parameter is const
4        std::cout << i << " ";
5    }(i);
6}

This shadows the mutable i with a const copy inside the lambda. The outer i is still mutable for the increment.

Solution 4: Separate Function

Extract the loop body into a function that takes const parameters:

cpp
1void process(const int index) {
2    // index = 5;  // Compile error
3    std::cout << "Processing " << index << "\n";
4}
5
6for (int i = 0; i < 10; ++i) {
7    process(i);
8}

This is the simplest refactoring — the function signature documents that the index is read-only.

Solution 5: Inner Scope with const Copy

Create a const copy inside the loop body:

cpp
1for (int i = 0; i < 10; ++i) {
2    const int idx = i;
3    // idx = 5;  // Compile error — idx is const
4    // i = 5;    // Still possible — not ideal
5    std::cout << idx << " ";
6}

This protects against accidental use of idx but does not prevent direct modification of i. It is a convention-based approach.

Solution 6: std::for_each with Index

cpp
1#include <algorithm>
2#include <vector>
3
4std::vector<int> data = {10, 20, 30, 40, 50};
5
6std::for_each(data.begin(), data.end(), [](const auto& value) {
7    // value = 99;  // Compile error — value is const
8    std::cout << value << " ";
9});

When you need the index:

cpp
1for (size_t i = 0; const auto& value : data) {
2    const auto idx = i++;
3    // idx = 5;  // Compile error
4    std::cout << idx << ": " << value << "\n";
5}

Other Languages

JavaScript (const in for-of)

javascript
1const arr = [10, 20, 30];
2for (const value of arr) {
3    // value = 99;  // TypeError — value is const
4    console.log(value);
5}
6
7// But traditional for loops cannot use const:
8// for (const i = 0; i < 10; i++) {}  // TypeError: Assignment to constant variable

Rust (Immutable by Default)

rust
1for i in 0..10 {
2    // i = 5;  // Compile error — i is immutable by default
3    println!("{}", i);
4}

Rust variables are immutable by default, so the loop variable is already protected. You need mut to make it mutable.

Go

go
1for i := 0; i < 10; i++ {
2    // No const loop variable in Go
3    // Convention: don't modify i in the body
4    fmt.Println(i)
5}
6
7// Range loop gives a copy each iteration
8for _, v := range items {
9    // v is a copy — modifying it doesn't affect the slice
10}

Common Pitfalls

  • const in traditional for loops: for (const int i = 0; i < 10; ++i) does not compile because ++i modifies i. The traditional for loop requires a mutable counter.
  • Performance of std::views::iota: std::views::iota is lazy and has zero allocation overhead. It is just as efficient as a raw for loop. Do not avoid it for performance reasons.
  • Lambda capture vs parameter: If you use [i] (capture by value) instead of taking const int i as a parameter, the captured i is const by default in the lambda body (unless you use mutable). But this is less explicit than a const parameter.
  • Shadowing confusion: The lambda/inner-scope approaches create a new variable that shadows the loop counter. This can be confusing if someone expects i and idx to be different values. Use clear naming.
  • C++20 availability: std::views::iota requires C++20 and a conforming standard library. If you are stuck on C++11/14/17, use a vector with std::iota or write a simple range() utility.

Summary

  • Use for (const int i : std::views::iota(0, n)) in C++20 for a truly const loop variable
  • Use for (const int i : range_vector) in C++11 with a pre-built vector
  • Extract the loop body into a function taking const int for a simple, portable solution
  • Use a lambda with a const parameter to shadow the mutable counter
  • In Rust, loop variables are immutable by default — no extra work needed
  • In JavaScript, for (const x of iterable) gives a const binding each iteration

Course illustration
Course illustration

All Rights Reserved.