www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Nested delegates and closure allocations

reply Anonymouse <zorael gmail.com> writes:
I'm increasingly using nested delegates to partition code.

```d
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:



 3. 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`?
Jan 16
parent reply FeepingCreature <feepingcreature gmail.com> writes:
On Tuesday, 16 January 2024 at 10:56:58 UTC, Anonymouse wrote:
 I'm increasingly using nested delegates to partition code.

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

     sendThing("bar", 42);
 }
 ```

 ...
 3. 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.
Jan 16
parent reply Anonymouse <zorael gmail.com> writes:
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? ```d 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). ```d 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?
Jan 16
parent reply FeepingCreature <feepingcreature gmail.com> writes:
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).

 ```d
 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.
Jan 16
parent Anonymouse <zorael gmail.com> writes:
On Tuesday, 16 January 2024 at 17:21:12 UTC, FeepingCreature 
wrote:
 Correct. [...]
Thanks, I think I understand.
Jan 17