Uncallable delegates
Dukc
ajieskola at gmail.com
Tue May 12 11:13:15 UTC 2026
On Tuesday, 12 May 2026 at 04:29:12 UTC, Walter Bright wrote:
>
> Thank you for your hard work on this. It's an important thing
> to get right.
For context to anyone else following: the feedback phase has
actually ended. The DIP is now in formal assessment and Walter is
asking for clarifications.
> The difficulty I have with it is I do not understand it. It has
> to be broken down into simpler constructs, then built back up
> into the delegate. How delegates behave is inevitable, it is
> something that we either get right or get severely wrong.
Let's start with your current intuition on delegates. You wrote
that your mental picture of a delegate is, roughly speaking, this:
```D
struct _delegate(FP) if (isFunctionPointer!FP)
{ // AddContextPointer is an imaginary template that returns a
function
// pointer passed to it with one parameter added for the
context pointer.
AddContextPointer!FP funPointer;
void* context;
auto opCall(Parameters!FP args) => funPointer(args, context);
}
```
This is all right and good as long as we're considering only
`@system` code. However, for `@safe` code there are additional
considerations.
First off, both `funPointer` and `context` are `@system` fields.
This means you can't access them directly in safe code, nor you
can't change them indepentently of each other, just like you
can't access or change the pointer of an array indepentely of
it's length.
Second, all safe delegates hold an important invariant: it is
safe to pass the context to the pointed function when calling it.
When the delegate is typed as `const` (for instance), it follows
the context is also `const`. Therefore the function behind
`funPointer` of any `const` delegate must not mutate the context
through the context pointer, otherwise the invariant is broken
and we have an unsafe delegate.
Now, there are some practically unavoidable corner cases where
such unsafe delegates can be created. Consider an abstract class:
```D
class A
{ abstract @safe void foo() const;
}
```
If you convert an object of `A` to `const(A)`, this could be
problematic, because the concrete class of the object could be
```D
class B : A
{ int field;
@safe void delegate() del;
@safe void increment(){field++;}
@safe void foo() const {del();}
}
```
and `del` might point `increment` and context to the object
itself. The result would be that calling `foo` mutates the object
in violation of the `const` qualification.
Because of this, the DIP specifies that if the delegate as a
whole has qualifiers that are incompatible with the context
qualification of the function pointer, the delegate cannot be
guaranteed to hold the invariant and thus cannot be called. In
this example, `B.foo` would fail to compile, because it's calling
a delegate typed `const(@safe void delegate())`. You would either
have to type `del` as `@safe void delegate() const` or remove
`const` qualification from `foo`.
Note that the opposite situation is fine. You are allowed to call
a mutable delegate pointing to a function that requires an
immutable context. This is safe because of the first invariant.
Only the function behind the pointer has access to the context
via the pointer in the delegate, so there's nothing that could
mutate the context through it (other than buggy `@trusted` code).
More information about the dip.development
mailing list