Thread overview
Nested delegates and closure allocations
January 16

I'm increasingly using nested delegates to partition code.

void foo(Thing thing)
{
    void sendThing(const string where, int i)
    {
        send(thing, where, i);
    }

    sendThing("bar", 42);
}

...where the nested sendThing sometimes returns something, sometimes doesn't. Thing may be a class or a value type, thing may be a parameter to the parent function, may be a variable previously declared in the parent function, may be mutable or immutable, may be modified inside sendThing; any combination of things. If sendThing doesn't need to access the scope of foo I mark it static to enforce that, but mostly it does.

From the spec:

>

19.19.2 Delegates & Closures

  1. Those referenced stack variables that make up the closure are allocated on the GC heap, unless:
  • The closure is passed to a scope parameter.
  • The closure is an initializer for a scope variable.
  • The closure is assigned to a scope variable.

I'm generally not storing the delegates or passing them around as values, so I don't think the thing about scope variables and parameters directly applies.

Am I safe as long as I don't do something like, pass &sendThing as an argument to std.concurrency.receive?

January 16

On Tuesday, 16 January 2024 at 10:56:58 UTC, Anonymouse wrote:

>

I'm increasingly using nested delegates to partition code.

void foo(Thing thing)
{
    void sendThing(const string where, int i)
    {
        send(thing, where, i);
    }

    sendThing("bar", 42);
}

...

>
  1. Those referenced stack variables that make up the closure are allocated on the GC heap, unless:
  • The closure is passed to a scope parameter.
  • The closure is an initializer for a scope variable.
  • The closure is assigned to a scope variable.

I'm generally not storing the delegates or passing them around as values, so I don't think the thing about scope variables and parameters directly applies.

Am I safe as long as I don't do something like, pass &sendThing as an argument to std.concurrency.receive?

Yes.

January 16

On Tuesday, 16 January 2024 at 13:45:22 UTC, FeepingCreature wrote:

> >

Am I safe as long as I don't do something like, pass &sendThing as an argument to std.concurrency.receive?

Yes.

Thank you.

And to make sure I don't misunderstand the spec; in the case I do have a delegate I want to pass elsewhere, and scope dg = &myFun; does compile, passing that dg around won't allocate a closure?

void foo(Thing thing) @nogc
{
    void sendThing(const string where, int i)
    {
        send(thing, where, i);
    }

    receiveTimeout(Duration.zero, &sendThing);
}

The above naturally won't compile because std.concurrency.receiveTimeout requires the garbage collector, but notably in the error message, this is included;

onlineapp.d(10): Error: function `onlineapp.foo` is `@nogc` yet allocates closure for `foo()` with the GC
onlineapp.d(12):        `onlineapp.foo.sendThing` closes over variable `thing` at onlineapp.d(10)

If I make a scope variable of the delegate and pass it to receiveTimeout, there no longer seems to be any mention of the closure in the error (given 2.092 or later).

void foo(Thing thing) @nogc
{
    void sendThing(const string where, int i)
    {
        send(thing, where, i);
    }

    scope scopeSendThing = &sendThing;
    receiveTimeout(Duration.zero, scopeSendThing);
}

Ignoring that it doesn't compile for other reasons; provided scope scopeSendThing = &sendThing; compiles -- as in, &sendThing is eligible for scope -- is this a valid workaround?

January 16

On Tuesday, 16 January 2024 at 15:39:07 UTC, Anonymouse wrote:

>

If I make a scope variable of the delegate and pass it to receiveTimeout, there no longer seems to be any mention of the closure in the error (given 2.092 or later).

void foo(Thing thing) @nogc
{
    void sendThing(const string where, int i)
    {
        send(thing, where, i);
    }

    scope scopeSendThing = &sendThing;
    receiveTimeout(Duration.zero, scopeSendThing);
}

Ignoring that it doesn't compile for other reasons; provided scope scopeSendThing = &sendThing; compiles -- as in, &sendThing is eligible for scope -- is this a valid workaround?

Correct. The problem is that receiveTimeout is defined as a template variadic function: it can take a scoped function, but it doesn't (can't) declare that its argument is always scoped, so since scoped parameters are opt-in, it defaults to unscoped. And &sendThing has to also default to unscoped, because you can pass unscoped values to scoped parameters but not the other way around, so it defaults to the most generic type available. With your scoped variable you provide DMD the critical hint that actually you want the closure to be scoped, and once the value is scoped it stays scoped.

January 17

On Tuesday, 16 January 2024 at 17:21:12 UTC, FeepingCreature wrote:

>

Correct. [...]

Thanks, I think I understand.