Constness and delegates

Mafi mafi at example.org
Thu Jan 9 23:11:48 UTC 2020


Regarding the work and comments on a pull request about delegate 
constness (https://github.com/dlang/dmd/pull/10644) I think there 
is some things we need to discuss.

I think the only sane way to analyze the soundness of delegates 
is to equate:

R delegate(P) qualifier f;

with:

interface I { R f(P) qualifier; }
I f;

Therefore given some class:

class C { R f(P) const; }
C c;

The delegate &c.f is of type 'R f(P) const'! The const qualifier 
does *not* apply to the this-Pointer of the delegate but to the 
contract of the invocation. Therefore the qualifier should be 
handled in a contravariant manner (and not covariant) because it 
describes the implicit this-Parameter of the referenced method 
and not the stored this-Pointer. The const-ness of the 
this-Pointer is the one "outside" the delegate.

qualifer1( R delegate(P) qualifier2 ) f;

Is always a well-formed type / variable. But f can only be called 
iff qualifier1 is implicitly convertible to qualifier2. This 
solves the soundness of delegates in const classes  and structs:

auto s = S(3);

struct S {
   int x = 0;
   void delegate() f;

   this(int x) { this.x = x; this.f = &this.incX; }
   void incX() { x++; }
   void const_method() const { f(); } // HERE
}

The line marked HERE compiles currently but f (in this const 
context) is of type 'const void delegate()' and therefore cannot 
be invoked (because const does not convert to mutable). If you 
change the type of f to "void delegate() const" it can be invoked 
but "incX" cannot be assigned to it! Soundness recovered. Const 
references cannot change any (implicit) state and immutable 
objects cannot observably change at all.

So in general:

struct S { void f() qualifier2; }
qualifier1 S s;
auto f = &s.f;

Is of type qualifier1(void delegate() qualifier2). Note the 
additional qualifier1 around the type. It (and not qualifier2) 
makes sure we respect the constness of the instance.

So what about implicit conversions? Well as always T -> const(T) 
<- immutable(T). Additionally qualifer(R delegate(P) const) -> 
qualifier(R delegate(P)), that is, we drop the const! This is 
because we loose power, the delegate cannot be invoked in const 
contexts anymore. This makes simple 'R delegate(P)' the goto-type 
for callbacks, as is probably the case anyways in most D code. Of 
course the inverse cannot be allowed, otherwise we lose the 
soundness again.

Additionally qualifier(R delegate(P) const) -> qualifier(R 
delegate(P) immtuble). This way immutable R delegate(P) immutable 
can be initialized from a delegate to const method on an 
immutable object.

Inline delegates that want to be const (either explicitely or 
maybe implicitely(?)) have to treat every referenced stack 
variable as const, like going through a const this-Pointer, which 
is actually what happens anyways.

I am not sure exactly how to treat inout. And I don't know in 
what state shared is in general. So what do you think? Does this 
sound reasonable? Please discuss.


More information about the Digitalmars-d mailing list