Tail Const (Was: I closed a very old bug!)

Simen Kjærås simen.kjaras at gmail.com
Fri Jan 19 22:34:00 UTC 2018


On Thursday, 18 January 2018 at 09:15:04 UTC, Simen Kjærås wrote:
> At any rate, this is a topic for a DIP.

So, a few more thoughts on this:

Arrays and pointers automatically decay to their Unqual'ed 
variants when passed to a templated function. AAs do not, which 
makes sense given their reference-type nature. Structs and 
classes don't, and in general shouldn't. Simple types, like ints 
and floats, don't.

As a matter of fact, Unqual is too blunt an instrument for what 
we want to do - Unqual!(const(MyStruct)) == MyStruct, regardless 
of what's inside MyStruct. For a struct with aliasing (one that 
contains pointers or arrays), that behavior is plain wrong for 
our use case.

What is actually needed here is a template that gives the 
equivalent head-mutable type, such that it can be put in a 
variable. For const(int), that's int. For const(int[]), int[]. 
For a const(MyStruct), it's const(MyStruct) or MyStruct, 
depending on whether MyStruct has aliasing. This test already 
exists in the compiler (try to assign a const struct {int[] arr; 
int i;} to a mutable variable, then try the same without the 
array).

This takes care of the non-templated problems, but the big issue 
is still ahead of us. For templates, as stated earlier, the 
connection between T and its head-mutable variant can be 
arbitrarily complex. However, a single function template is all 
that's needed to convey all the necessary information. Let's call 
it opDecay, and give this implementation of the basic logic:

template Decay(T)
{
     import std.traits : Unqual, hasAliasing, isAssociativeArray;
     static if (is(typeof(T.init.opDecay())))
     {
         alias Decay = typeof(T.init.opDecay());
     }
     else static if (is(T == class) || isAssociativeArray!T ||
                    (is(T == struct) && hasAliasing!(Unqual!T)))
     {
         alias Decay = T;
     }
     else
     {
         alias Decay = Unqual!T;
     }
}

unittest
{
     // Regular types:
     assert(is(Decay!(const int) == int));
     assert(is(Decay!(const int*) == const(int)*));
     assert(is(Decay!(const int[]) == const(int)[]));
     assert(is(Decay!(const int[10]) == int[10]));
     assert(is(Decay!(const int[int]) == const(int[int])));

     // Struct without aliasing:
     static struct S1 {
         int n;
         immutable(int)[] arr;
     }
     assert(is(Decay!(const S1) == S1));

     // Struct with aliasing:
     static struct S2 {
         int[] arr;
     }
     assert(is(Decay!(const S2) == const S2));

     // Struct with aliasing, with opDecay hook:
     static struct S3 {
         int[] arr;
         S3 opDecay(this T)() {
             return S3(arr.dup);
         }
     }
     assert(is(Decay!(const S3) == S3));

     // Templated struct with aliasing, with opDecay hook
     struct S4(T) {
         T[] arr;
         auto opDecay(this This)() const {
             import std.traits : CopyTypeQualifiers;
             return S4!(CopyTypeQualifiers!(This, T))(arr.dup);
         }
     }
     assert(is(Decay!(const S4!int) == S4!(const int)));
}

We now have a way of obtaining head-mutable variables of any type 
that supports it. Alias this takes care of implicit conversion to 
the decayed type ...except when it doesn't. As stated above, 
arrays decay when passed to a templated function:

import std.range;

void foo(T)(T arr) {
     assert(is(T == const(int)[]));
     assert(isInputRange!T);
}
unittest {
     const(int[]) a;
     foo(a);
     assert(!isInputRange!(typeof(a)));
}

This behavior is special - no types other than T* and T[] decay 
in this way, and there's no way to tell the compiler you want 
your type to do the same. Is it important that our types do the 
same? I'm not entirely sure. The fact that this decay is 
inconsistent[1] today makes me even less sure. If we want the 
same kind of behavior for user-defined types, the compiler will 
need to insert calls to opDecay when a type with that method is 
passed to a function[2].

opDecay is likely to be a small function that can be inlined and 
in many cases elided altogether, and will only be used for a 
small subset of types, so I believe the overhead of calling it 
whenever a type with the method is passed to another function, 
would be negligible.

Destroy!

--
   Simen

[1]: https://issues.dlang.org/show_bug.cgi?id=18268
[2]: If typeof(x.opDecay) != typeof(x), and the type of the 
parameter is not explicitly typeof(x).


More information about the Digitalmars-d mailing list