`in` on function parameters: const, scope const, ref scope const ?

tsbockman thomas.bockman at gmail.com
Sun Apr 5 14:40:49 UTC 2020


On Sunday, 5 April 2020 at 12:54:01 UTC, Petar Kirov [ZombineDev] 
wrote:
> On Sunday, 5 April 2020 at 02:01:05 UTC, tsbockman wrote:
>> Given the possibility of aliasing, adding `ref` to parameters 
>> that are merely `const`, but not `immutable`, could break 
>> existing code in very subtle ways.
>
> Interesting, I wasn't aware of this. Can you elaborate more?

`const` marks a read-only view of some data, for which there 
*may* currently be mutable aliases, as well. `immutable` means 
that all current aliases for the data are read-only, and so it is 
guaranteed not to change as long as the type system is not 
subverted through unsafe casting.

import std.stdio;

bool isPositive(in int x, scope ref int callCount) pure @safe {
     callCount += 1;
     return (x > 0);
}

void main() @safe {
     int a = 0;
     writeln(isPositive(a, a));
}

The above should print "true" if `in` means `ref scope const`, 
and "false" if `in` means just `scope const`. This minimal 
example is completely contrived, of course, but the issue does 
appear in reasonable real world code, too.

Aliasing can occur via `ref`, but also via class references, 
pointers, delegates, array slices, and user-defined data 
structures containing or referencing any of those things at any 
level of indirection. And, those are just things that need to be 
tracked to detect aliasing in a program that is 100% @safe code; 
with @trusted, there is the possibility that additional aliases 
may be encoded, and correctly managed, in arbitrary data types 
that are not understood as pointer types by the semantic analyzer:

import std.stdio;

struct CustomRef {
     private size_t address;
     void opAssign(int* target) pure @trusted nothrow @nogc {
         address = cast(size_t) target;
     }
     ref inout(int) access() inout pure @trusted nothrow @nogc {
         return *cast(inout(int)*) address;
     }
}

bool isPositive(ref scope const int x, scope ref int callCount) 
pure @safe {
     callCount += 1;
     return (x > 0);
}

int a = 0;
CustomRef q;

void main() @safe {
     q = &a;
     writeln(isPositive(a, q.access));
}

Again, this is a contrived example, and simple enough that the 
semantic analyzer could no doubt be extended to catch it. But, 
there are valid practical applications for code like this, some 
of which may be far more difficult, or even impossible, to 
analyze automatically in the general case.

So, changing `in` to be pass-by-ref is, at least in theory, a 
very serious breaking change - *far* more so than adding `scope`, 
which was always planned anyway. If we're going to do this I 
think a long deprecation cycle is needed for the existing 
behavior, with aggressive warnings about the upcoming change.

I don't think it will break very much code in the real world, 
it's just that some of the code it does break will probably fail 
in such a subtle way, for certain inputs only, that it will be a 
real pain to debug.


More information about the Digitalmars-d mailing list