Pitfalls of delegates inside ranges

Joseph Rushton Wakeling joseph.wakeling at webdrake.net
Sun Sep 1 15:27:44 PDT 2013


Hello all,

I came across an interesting little pitfall of delegates recently which I 
thought I'd share.

TL;DR: having reference types as struct members can be dangerous. ;-)

Suppose we have a fairly simple range which is supposed to count from 0 to some 
maximum.  The trick is that the count is implemented by a function that's 
accessed by a delegate.

struct Foo
{
     private size_t _count = 0, _max;
     private void delegate() jump = null;

     @disable this();

     this(size_t max)
     {
         _max = max;
         jump = &jump10;
     }

     bool empty() @property @safe pure const nothrow
     {
         return (_count > _max);
     }

     size_t front()
     {
         return _count;
     }

     void popFront()
     {
         if (empty)
         {
             return;
         }

         writeln("At start of popFront(), count = ", _count);
         writeln("Jumping ...");
         jump();
         writeln("At end of popFront(), count = ", _count);
     }

     void jump10()
     {
         _count += 10;
         writeln("At end of jump, count = ", _count);
     }
}

Now let's put this struct to use:

     auto foo = Foo(50);

     foreach (f; foo)
     {
         writeln(f);
     }

What we expect is that the program will print out 0, 10, 20, 30, 40, 50 (on new 
lines) and exit.

In fact, the foreach () loop never exits and the logging writeln's we put in 
tell us a strange story:

0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 10
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 20
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 30
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 40
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 50
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 60
At end of popFront(), count = 0
0
At start of popFront(), count = 0
Jumping ...
At end of jump, count = 70
At end of popFront(), count = 0

... and so on.  It seems like the value of _count is never being incremented. 
The logging function inside jump10 keeps telling us: "At end of jump, count = 
70" (and 80, and 90, and ... 30470 ... and ...), but front() and popFront() keep 
telling us the count is zero.

Of course, it's because of the delegate.  The foreach () loop has taken a 
(value-type) copy of the Foo struct, but the delegate jump() is a reference -- 
which is pointing to the function jump10 in the _original_ Foo, not the copy 
that foreach () is operating on.

Hence, it's the original's _count that's being incremented, and not that in 
foreach () loop's copy -- and so the foreach loop never exits.

If you're interested in the _real_ place I discovered this, see:
https://github.com/D-Programming-Language/phobos/pull/1533

... and more specifically:
https://github.com/WebDrake/phobos/commit/e6b7e1f4c0ac63417c78f2d916fe5ada871250bb

... where I thought I'd discovered a quite nice little solution that would speed 
up RandomSample in some scenarios.  But alas, not to be. :-(

Best wishes,

     -- Joe


More information about the Digitalmars-d-learn mailing list