Pitfalls of delegates inside ranges

Artur Skawina art.08.09 at gmail.com
Mon Sep 2 06:39:39 PDT 2013


On 09/02/13 00:27, Joseph Rushton Wakeling wrote:
> 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:

> ... 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.

    this(this) { jump.ptr = &this; }

While this may not seem ideal, sometimes you do *not* want to do it
(when you need to keep the original context), so there are no obvious
alternatives that would handle all cases automagically.

artur


More information about the Digitalmars-d-learn mailing list