casting away const and then mutating

Steven Schveighoffer via Digitalmars-d digitalmars-d at puremagic.com
Fri Jul 24 13:08:10 PDT 2015


On 7/24/15 3:35 PM, Jonathan M Davis wrote:
> On Friday, 24 July 2015 at 14:07:46 UTC, Steven Schveighoffer wrote:
>> On 7/23/15 11:58 PM, Jonathan M Davis wrote:
>>> On Friday, 24 July 2015 at 03:02:30 UTC, Steven Schveighoffer wrote:
>>>> Basically, if we say this is undefined behavior, then inout is
>>>> undefined behavior.
>>>
>>> inout is done by the compiler. It knows that it's safe to cast the
>>> return type to mutable (or immutable), because it knows that the return
>>> value was either the argument that it passed in or something constructed
>>> within the function and thus safe to cast. The compiler knows what's
>>> going on, so it can ensure that it doesn't violate the type system and
>>> is well-defined.
>>
>> The compiler knows everything that is going on inside a function. It
>> can see the cast and knows that it should execute it, and also that
>> the original variable is mutable and could be the one being mutated.
>> This isn't any different.
>
> You're assuming that no separate compilation is going on, which in the
> case of a template like you get with RedBlackTree, is true, because the
> source has to be there, but in the general case, separate compilation
> could make it so that the compiler can't see what's going on inside the
> function.

No, I'm not. Using an inout function is like inserting a wrapper around 
the real function that casts the result back to the right type. The 
inout rules inside the function make the casting sane without having to 
examine the code, but the code itself inside does not do any casting.

> It relies on the separate compilation step having verified the
> inout attribute appropriately when the function's body was compiled, and
> all it has to go on is the inout in the signature. If you were to try
> and implement inout yourself in non-templated code, the compiler
> wouldn't necessarily be able to see any of what's going on inside the
> function when it compiles the calling code. Without inout, in
> non-templated code, even if the compiler were being very smart about
> this, it wouldn't have a clue that when you cast away const on the
> return value that it was the same one that was passed in.

The compiler doesn't see "inout", it sees mutable, const, immutable -- 
three versions of the function. It calls the right one, and inside the 
function, the casting happens outside the implementation. The compiler 
doesn't have to know it's the same value, it doesn't even have to care 
whether the value is modified. It just has to accept that it can't 
optimize out the loading of the mutable variable again.

In other words, if the compiler compiles this:

int *foo(int *x);

void main()
{
    int x;
    auto y = foo(&x);
    y = 5;
}

It doesn't have to know that y is or is not pointing at x. What it just 
knows is that x may have changed inside foo, and that it's possible y is 
pointing at it (and therefore changed it as well).

>>> All of the lines with pureFunc* could be removed outright, because
>>> they're all pure function calls, and they can't possibly have mutated
>>> myFoo. I wouldn't expect a lot of dead code like that, and maybe
>>> something like that would never be implemented in the compiler, but it
>>> could be as long as the compiler can actually rely on const not being
>>> mutated.
>>
>> And my interpretation of the spec doesn't change this. You can still
>> elide those calls as none of them should be casting away const and
>> mutating internally.
>
> You seem to be arguing that as long as you know that a const reference
> refers to mutable data, it is defined behavior to cast away const and
> mutate it.

No. If you *create* a const reference to mutable data, you can cast away 
that const back to mutable, because everything is there for the compiler 
to see.

> And if that were true, then if you knew that myFoo referred
> to mutable data, it would be valid to cast away const and mutate it
> inside of one of the pureFunc* functions, because you know that it's
> mutable and not immutable.

No, because those pure functions don't know whether the data is mutable, 
and the compiler is allowed to infer that they don't based on their 
signatures.

Basically, it's the difference between these 2 calls:

pure void foo(int *x) { *x = 5;}
pure void bar(const(int) *x) { *(cast(int *)x) = 10;}

void main()
{
    int x;
    const int *y = &x;
    foo(cast(int *)y); // should be OK, can't be elided, and the 
compiler can see what is going on here
    bar(y); // BAD, compiler is free to remove
}

> And this shows why that isn't enough. And
> that is the major objection I have with what you're arguing here. In the
> general case, even if immutable is not used in the program even once,
> casting away const and mutating is not and cannot be defined behavior,
> or const guarantees nothing - just like in C++.
>
> The exact use case that you're looking for - essentially inout - works
> only because when you cast it back, no const reference that was
> generated by calling the function with a mutable reference escaped that
> function except via the return value, so there's no way for the compiler
> to optimize based on the const reference, because there isn't one
> anymore. And that's _way_ more restricted than saying that it's defined
> behavior to cast away const and mutate as long as you know that the
> underlying data is actually mutable.

I'm not saying that general statement. I'm saying in restricted 
situations, casting away const is not undefined behavior.

> Your case works, because the const reference is gone after the cast, and
> there are no others that were created from the point that it temporarily
> became const. So, it's a very special case. And maybe the rule can be
> worded in a way that incorporates that nicely, whereas simply saying
> that it's undefined behavior to cast away const and mutate would not
> allow it. But we cannot say that it's defined behavior to cast away
> const and mutate simply because you know that the data is mutable, or we
> do not have physical const, and const provides no real guarantees.

I agree, we can't just make the general case that you can cast away 
const if you know the data is mutable, given some configuration of 
function calls. There has to be complete visibility to the compiler 
within the same function to allow the possibility that some mutable data 
changed.

We can start with "casting away const and mutating, even if you know the 
underlying data is mutable, is UB, except for these situations:..."

And relax from there.

-Steve


More information about the Digitalmars-d mailing list