Search⌘ K
AI Features

Loop Counter

Explore how to add loop counters in D foreach loops for user-defined types. Understand how enumerate from std.range works with range member functions and how opApply can be overloaded to support counters. This lesson teaches you to handle iterations safely without mutating collections during loops.

The convenient loop counter of slices is not automatic for other types. Loop counter can be achieved for user-defined types in different ways depending on whether the foreach support is provided by range member functions or by opApply overloads.

Loop counter with range functions

If foreach support is provided by range member functions, then a loop counter can be achieved simply by enumerate from the std.range module:

import std.range; 

// ...

foreach (i, element; NumberRange(42, 47).enumerate) {
    writefln("%s: %s", i, element); 
}

enumerate is a range that produces consecutive numbers starting by default from 0. enumerate pairs each number with the elements of the range that it is applied on. As a result, the numbers that enumerate generates and the elements of the actual range (NumberRange in this case) appear in lockstep as loop variables:

0: 42
1: 43
2: 44
3: 45
4: 46

Loop counter with opApply

On the other hand, if foreach support is provided by opApply(), the loop counter must be defined as a separate parameter of the delegate, suitably as type size_t. Let’s see this on a struct that represents a colored polygon.

As you know, an opApply() that provides access to the points of this polygon can be implemented without a counter as in the following code:

svg viewer
D
import std.stdio;
enum Color { blue, green, red }
struct Point {
int x;
int y;
}
struct Polygon {
Color color;
Point[] points;
int opApply(int delegate(ref const(Point)) dg) const {
int result = 0;
foreach (point; points) {
result = dg(point);
if (result) {
break;
}
}
return result;
}
}
void main() {
auto polygon = Polygon(Color.blue,[ Point(0, 0), Point(1, 1) ] );
foreach (point; polygon) {
writeln(point);
}
}

Note that opApply() itself is implemented by a foreach loop. As a result, the foreach inside main() ends up making indirect use of a foreach over the points member (Line 15).

Also, note that the type of the delegate parameter is ref const(Point). This means that this definition of opApply() does not allow modifying the Point elements of the polygon. In order to allow user code to modify the elements, both the opApply() function and the delegate parameter must be defined without the const specifier.

Naturally, trying to use this definition of Polygon with a loop counter would cause a compilation error:

D
import std.stdio;
enum Color { blue, green, red }
struct Point {
int x;
int y;
}
struct Polygon {
Color color;
Point[] points;
int opApply(int delegate(ref const(Point)) dg) const {
int result = 0;
foreach (point; points) {
result = dg(point);
if (result) {
break;
}
}
return result;
}
}
void main() {
auto polygon = Polygon(Color.blue,[ Point(0, 0), Point(1, 1) ] );
foreach (i, point; polygon) { // ← compilation ERROR
writefln("%s: %s", i, point);
}
}

For this to work, another opApply() overload that supports a counter must be defined:

D
import std.stdio;
enum Color { blue, green, red }
struct Point {
int x;
int y;
}
struct Polygon {
Color color;
Point[] points;
int opApply(int delegate(ref const(Point)) dg) const {
int result = 0;
foreach (point; points) {
result = dg(point);
if (result) {
break;
}
}
return result;
}
int opApply(int delegate(ref size_t,
ref const(Point)) dg) const {
int result = 0;
foreach (i, point; points) {
result = dg(i, point);
if (result) {
break;
}
}
return result;
}
}
void main() {
auto polygon = Polygon(Color.blue,[ Point(0, 0), Point(1, 1) ] );
foreach (i, point; polygon) {
writefln("%s: %s", i, point);
}
}

This time, the foreach variables are matched to the new opApply() overload, and the program prints the desired output.

Note that this implementation of opApply() takes advantage of the automatic counter over the points member. (Although the delegate variable is defined as ref size_t, the foreach loop inside main() cannot modify the counter variable over points.)

When needed, the loop counter can also be defined and incremented explicitly. For example, because the following opApply() is implemented by a while statement, it must define a separate variable for the counter:

int opApply(int delegate(ref size_t,
                         ref const(Point)) dg) const {
    int result = 0;
    bool isDone = false;
    
    size_t counter = 0; 
    while (!isDone) {
        // ...

        result = dg(counter, nextElement);
        
        if (result) {
            break; 
        }
        ++counter; 
    }
    return result;
}

Warning: The collection must not mutate during the iteration

Regardless of whether the iteration support is provided by the range member functions or by opApply() functions, the collection itself must not mutate. New elements must not be added to the container, and the existing elements must not be removed. Mutating the existing elements is allowed. Doing otherwise is undefined behavior.