opApply Magic Function Body Transformation
Mike Shah
mshah.475 at gmail.com
Mon Jul 28 04:39:00 UTC 2025
I'm preparing a video to teach opApply, and I think I'm still
kind of unsure on how opApply works in regards to the compiler
transformation. It's one of those features I understand how to
use, but I'd like a bit of a deeper understanding before I teach
it -- to admittingly really know what I am doing. Provided is
something I quickly wrote using opApply as an example, and a few
concrete questions bolded below if you want to skip ahead.
```d
import std.stdio;
struct Array(T){
T[] array;
int opApply(int delegate(ref T) dg){
int result;
for(int i=0; i < array.length; i++){
result = dg(array[i]);
if(result){
break;
}
}
return result;
}
}
void main(){
Array!int ints;
ints.array = [4,6,8,10,12];
foreach(item; ints){
writeln(item);
}
}
```
The purpose of opApply I am clear on -- it's a member function
for 'foreach/foreach_reverse' loops for use in iteration. It
takes priority over range member functions if both are defined,
and otherwise *maybe* has some performance trade-offs (or at the
least, it's slightly easier to template one member function
versus 3 for an inputRange to avoid virtual calls -- but that's
an aside that needs testing). Okay -- but now onto the part where
I need some more understanding -- the delegate and the
transformation.
In my understanding/teaching of opApply, I would break opApply
into two main concepts:
1.) The operator overloading of 'opApply' -- and the requirement
that opApply always returns an integer on the member function
signature.
2.) The single parameter to opApply must otherwise be a delegate
parameter. The paramaters to the delegate otherwise match the
'foreach' parameters. This portion also has more to do with the
magic transform I am not 100% clear on.
The second part (delegate parameter) is what I'm interested in
being able to visualize. So in the above code, there's two sort
of steps going on:
First, the transformation of a foreach loop being lowered to a
regular 'for' loop.
```d
foreach(item; ints){
writeln(item);
}
// is re-written to something-like what is below.
// But we don't really know if it's 'ints.array.length' or some
other field.
// Thus we rely on our overload (which we can have multiple) of
opApply
// to sort of figure this out..
{
int i=0;
for(; i != ints.array.length; i++){
writeln( /* item */ ); // 'item' represents whatever the
delegate parameter
// is i.e. 'ref T' above.
// The 'T' is also the item I am
iterating on, and
// performing some computation on.
}
}
```
The next transformation I can *kind of* see if I use 'dmd vcg-ast
main.d' to compile. I can see the delegate and some magic
'__applyArg0'.
```d
23 void main()
24 {
25 Array!int ints = 0;
26 ints.array = [4, 6, 8, 10, 12];
27 cast(void)ints.opApply(delegate int(ref int __applyArg0)
@safe => 0);
28 return 0;
29 }
```
**So really my one concrete question is** -- can I see
main.main()__foreachbody_L21_C3(ref int) anywhere? This appears
to be either a function or label that is the body of my loop. As
you'll notice, on line 27, there is no longer a 'foreach' loop
anymore. But I don't seem to be able to see the transformation,
or otherwise find the symbols anywhere. Perhaps there is another
magic compiler flag I am missing?
**My second concrete question is** When learning opApply, is it
useful to think of the 'work' being done as a copy and paste of
the work being done in a 'foreach ' loop being pasted in? Or
perhaps just to look at `cast(void)ints.opApply(delegate int(ref
int __applyArg0) @safe => 0);` and understand your code has been
magically transformed? Some of my intuition in comments is below.
```d
// Somewhere in main
27 cast(void)ints.opApply(delegate int(ref int __applyArg0)
@safe => 0);
// "Sort of" what is going on.
// At least to provide some mental model
// for the Array.opApply implementation.
int opApply(delegate int(ref int __applyArg0) dg)(
{
int result;
for(int i=0; i < this.array.length; i++){
auto result = dg(this.array[i]);
// {
// 'dg' represents the original 'foreach'
loop
// effectively copied here.
// -- but it's not a 'copy and paste of
code here',
// instead we have a call to a magic
delegate
// that exists somewhere from compiler.
// I can *think* of the delegate like
pasting
// in the body of original foreach loop,
but only one
// item at a time -- considering the
single 'index' (elem)
// from our loop.
// writeln(elem); // same work, but this
work comes from
// body of original 'foreach' found in
'main()' now wrapped
// in a delegate function.
//};
if(result){ // Returns '0' from magic delegate at some
point?
break;
}
}
return result;
}
```
**A third question** Will this call to a delegate provide more
hidden allocations I wonder?
---
Some more investigation
The disassembly (I used gdc-14 to build and then disassemble with
'objdump -d main_binary') seems to generate something like this.
In 'C' parlance we have a function pointer otherwise representing
where the 'foreach_body' in main otherwise would be stored
somewhere. It also appears (both from the disassembly, and from
vcg-ast) that the return value of '1' or '0' does not seem
important?
```c
void opApply(int* array, int size, void (*func)(int)) {
int index = 0;
int result = 0;
while (index < size) {
if (index < size) {
func(array[index]);
result = 1;
}
index++;
}
}
```
---
Sorry if my questions are not clear, any guidance or pointers to
examples are helpful!
More information about the Digitalmars-d-learn
mailing list