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