Programming in D - static foreach

Programming in D – Tutorial and Reference
Ali Çehreli

static foreach

We saw compile-time foreach earlier in the Tuples chapter. Compile-time foreach iterates the loop at compile time and unrolls each iteration as separate pieces of code. For example, given the following foreach loop over a tuple:

 auto t = tuple(42, "hello", 1.5);
 foreach (i, member; t) {
 writefln("%s: %s", i, member);
 }

The compiler unrolls the loop similar to the following equivalent code:

 {
 enum size_t i = 0;
 int member = t[i];
 writefln("%s: %s", i, member);
 }
 {
 enum size_t i = 1;
 string member = t[i];
 writefln("%s: %s", i, member);
 }
 {
 enum size_t i = 2;
 double member = t[i];
 writefln("%s: %s", i, member);
 }

Although being very powerful, some properties of compile-time foreach may not be suitable in some cases:

  • With compile-time foreach, each unrolling of the loop introduces a scope. As seen with the i and member variables above, this allows the use of a symbol in more than one scope without causing a multiple definition error. Although this can be desirable in some cases, it makes it impossible for code unrolled for one iteration to use code from other iterations.
  • Compile-time foreach works only with tuples (including template arguments in the form of AliasSeq). For example, despite the elements of the following array literal being known at compile time, the loop will always be executed at run time (this may very well be the desired behavior):
void main() {
 enum arr = [1, 2];
 // Executed at run time, not unrolled at compile time:
 foreach (i; arr) {
 // ...
 }
}
  • Like regular foreach, compile-time foreach can only be used inside functions. For example, it cannot be used at module scope or inside a user-defined type definition.
import std.meta;
// Attempting to define function overloads at module scope:
foreach (T; AliasSeq!(int, double)) { // ← compilation ERROR
 T twoTimes(T arg) {
 return arg * 2;
 }
}
void main() {
}
Error: declaration expected, not foreach
  • With compile-time foreach, it may not be clear whether break and continue statements inside the loop body should affect the compile-time loop iteration itself or whether they should be parts of the unrolled code.

static foreach is a more powerful compile-time feature that provides more control:

  • static foreach can work with any range of elements that can be computed at compile time (including number ranges like 1..10). For example, given the FibonacciSeries range from the Ranges chapter and a function that determines whether a number is even:
 static foreach (n; FibonacciSeries().take(10).filter!isEven) {
 writeln(n);
 }

The loop above would be unrolled as the following equivalent:

 writeln(0);
 writeln(2);
 writeln(8);
 writeln(34);
  • static foreach can be used at module scope.
  • static foreach does not introduce a separate scope for each iteration. For example, the following loop defines two overloads of a function at module scope:
import std.meta;
static foreach (T; AliasSeq!(int, double)) {
 T twoTimes(T arg) {
 return arg * 2;
 }
}
void main() {
}

The loop above would be unrolled as its following equivalent:

 int twoTimes(int arg) {
 return arg * 2;
 }
 double twoTimes(double arg) {
 return arg * 2;
 }
  • break and continue statements inside a static foreach loop require labels for clarity. For example, the following code unrolls (generates) case clauses inside a switch statement. The break statements that are under each case clause must mention the associated switch statements by labels:
import std.stdio;
void main(string[] args) {
theSwitchStatement:
 switch (args.length) {
 static foreach (i; 1..3) {
 case i:
 writeln(i);
 break theSwitchStatement;
 }
 default:
 writeln("default case");
 break;
 }
}

After the loop above is unrolled, the switch statement would be the equivalent of the following code:

 switch (args.length) {
 case 1:
 writeln(1);
 break;
 case 2:
 writeln(2);
 break;
 default:
 writeln("default case");
 break;
 }

AltStyle によって変換されたページ (->オリジナル) /