[Issue 1983] Delegates violate const

d-bugmail at puremagic.com d-bugmail at puremagic.com
Wed Feb 10 00:50:55 UTC 2021


https://issues.dlang.org/show_bug.cgi?id=1983

--- Comment #28 from Bolpat <qs.il.paperinik at gmail.com> ---
(In reply to timon.gehr from comment #27)
It took me some time to think about and come to something like conclusion. Some
parts may look like we [Timon Gehr and me] agree, but I'm not entirely sure we
are. My mental model of a delegate is

    struct DG(R, Args...)
    {
        alias Context = void*;
        alias FP = R function(Context, Args);
        FP fp;
        Context context;

        this(FP fp, Context context)
        {
            this.fp = fp;
            this.context = context;
        }

        R opCall(Args args)
        {
            return fp(context, args);
        }
    }

too. However, I make myself aware that this is an implementation detail and in
principle, other implementations are possible where the context pointer is not
actually part of the delegate object but externalized to a global associative
array. One would be this:

    struct DG(R, Args...)
    {
        alias Context = void*;
        alias FP = R function(Context, Args);

        FP fp;
        static Context[FP] contexts;

        this(FP fp, Context context)
        {
            this.fp = fp;
            contexts[fp] = context;
        }
        ~this()
        {
            // contexts.remove(fp);
        }

        R opCall(Args args)
        {
            return fp(contexts[fp], args);
        }

As someone in the forums in the discussion of `const` and `immutable` pointed
out, stuff that is conceptually part of an object need not literally be part of
that object. If `DG` is a delegate type, having `const(DG)` mean the context
must not be mutated by a call to that delegate is a perception that comes from
seeing a delegate as a functionPtr-context pair.

Thanks to your [Timon Gehr's] explanations (the formal stuff wasn't so easy to
comprehend, to be honest), I now do not see annotations for how the delegate
can do things with its context as a problem now. I see immutable contexts as an
absolute win: A pure delegate makes almost no practically useful guarantees,
but a pure immutable one does.

Now I feel the need to invent terminology:
A const/immutable/... [annotated] delegate has those restrictions on how its
context pointer is affected by it.
A const/immutable/... (type|qualified) delegate cannot be assigned.

I will use "type" or "qualified" always, but "annotated" only for emphasis. The
annotations are of the same sort as `pure` and `nothrow` and describe the
behavior of the function pointer with respect to the value. Type qualifiers
describe the variable.
As an example, an `immutable` annotated delegate is

    alias DGa = void delegate(int) immutable

and a ´const` qualified delegate is

    alias DGb = const(void delegate(int))

And those annotations have nothing to do with each other. They mean orthogonal
things. A DGa object can be reassigned and calling a DGb object can mutate
stuff through its context.

Following the example class, I don't think a `const` method should be barred
from calling a mutable annotated delegate on the same reasoning it isn't barred
from calling mutable methods of other classes. Most likely, the delegate's
context has nothing to do with the class. (We made it to prove a point.)
Barring a `const` method of a class to only call a `const` annotated member
delegate is a major restriction.

In the example, we had a `pure` constructor that assigns the address of a
mutable method to a mutable delegate member. This is valid, obviously, it's an
exact type match. Using the second model of handling contexts with an external
AA, it proves that a reference is leaked. That leaked reference invalidates
uniqueness. That means the implicit cast to immutable is invalid, too.
However, the result is unique when the delegate were annotated `immutable` (or
`const`?). Then, the assignment of a MUTABLE member function wouldn't be valid.

If we tried to replace the pure constructor by two (or three) ones for explicit
mutable and immutable (and const) construction, it becomes more obvious that if
an aggregate type contains a mutable annotated (i.e. not `const` or `immutable`
or `inout` annotated) delegate, even the result of a `pure` constructor cannot
be assumed to yield unique values.

I don't think that an `immutable` (or `const`) delegate type variable should
extend the qualification down to its context as it would be a breaking change.
It would have to if your model is the function-ptr--context pair, but I think
this model is misleading. The qualification of the delegate object should only
affect assignment of the function ptr and the context, but not references
reachable through that context. If it were, a delegate with a context that
binds non-`immutable` values cannot be assigned to an `immutable` qualified
delegate type variable.
The annotation of a delegate should only affect the signature of the function
pointer: The context parameter is mutable, const or immutable depending on the
annotation. It tells what the function pointer does, not what the context is.

--


More information about the Digitalmars-d-bugs mailing list