Thread overview
Consequences of casting away immutable from pointers
Jan 05, 2018
jmh530
Jan 05, 2018
Adam D. Ruppe
Jan 05, 2018
Jonathan M Davis
Jan 05, 2018
Jonathan M Davis
Jan 05, 2018
jmh530
Jan 05, 2018
jmh530
Jan 05, 2018
H. S. Teoh
Jan 05, 2018
Patrick Schluter
January 05, 2018
I'm trying to understand the consequences of casting away immutable from a pointer. The code below has some weird things going on like the pointers still point to the correct address but when you dereference them they don't point to the correct value anymore.

Should I just assume this is undefined behavior and not bother with it? Or is there a use case for this?


void main()
{
    immutable(int) x = 5;
    auto p_x = &x;
    int* p_x_alt = cast(int*)p_x;
    (*p_x_alt)++;

    //addresses remain unchanged
    assert(&x == p_x);
    assert(p_x == p_x_alt);

    assert(*p_x_alt == 6);
    assert(*p_x == *p_x_alt); //but p_x and p_x_alt point to same value
    assert(x != *p_x); //yet that is not the case for x
    assert(x == 5); //which still is 5
}
January 04, 2018
On 1/4/18 10:58 PM, jmh530 wrote:
> I'm trying to understand the consequences of casting away immutable from a pointer. The code below has some weird things going on like the pointers still point to the correct address but when you dereference them they don't point to the correct value anymore.
> 
> Should I just assume this is undefined behavior and not bother with it? Or is there a use case for this?

Yes, this is undefined behavior.

https://dlang.org/spec/const3.html#removing_with_cast

The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).

The compiler would likely be free to assume *p_x == 5 forever also, if it was clever enough.

I'd recommend not doing this.

-Steve
January 05, 2018
On Friday, 5 January 2018 at 04:10:54 UTC, Steven Schveighoffer wrote:
> The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).

I was curious what dmd did, and the disassembly indeed shows it just loads 5 into the register and leaves it there - assuming since it is immutable, it will never change through any pointer and thus never reloads it from memory at any time.

Interestingly, dmd -O just stubs out the whole function. I guess it assumes all the defined behavior actually accomplishes nothing and it is free to optimize out undefined behavior... thus the function needs no code. Similarly,  if the last assert is changed to x != 5, dmd -O doesn't even actually do a comparison (the value 5 never appears in the generated code!), it just outputs the direct call to assertion failure.
January 04, 2018
On Thursday, January 04, 2018 23:10:54 Steven Schveighoffer via Digitalmars- d-learn wrote:
> On 1/4/18 10:58 PM, jmh530 wrote:
> > I'm trying to understand the consequences of casting away immutable from a pointer. The code below has some weird things going on like the pointers still point to the correct address but when you dereference them they don't point to the correct value anymore.
> >
> > Should I just assume this is undefined behavior and not bother with it? Or is there a use case for this?
>
> Yes, this is undefined behavior.
>
> https://dlang.org/spec/const3.html#removing_with_cast
>
> The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).
>
> The compiler would likely be free to assume *p_x == 5 forever also, if it was clever enough.
>
> I'd recommend not doing this.

Yeah, casting away either const or immutable is just begging for trouble, though it's likely to be worse with immutable, since there are more optimizations that the compiler can do based on immutable. D's const and immutable are definitely not the same as C++'s const and treating either of them like they have backdoors is just going to cause bugs. If you ever need a backdoor to get around them, then you shouldn't be using them.

- Jonathan M Davis

January 04, 2018
On Friday, January 05, 2018 04:16:48 Adam D. Ruppe via Digitalmars-d-learn wrote:
> On Friday, 5 January 2018 at 04:10:54 UTC, Steven Schveighoffer
>
> wrote:
> > The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).
>
> I was curious what dmd did, and the disassembly indeed shows it just loads 5 into the register and leaves it there - assuming since it is immutable, it will never change through any pointer and thus never reloads it from memory at any time.
>
> Interestingly, dmd -O just stubs out the whole function. I guess it assumes all the defined behavior actually accomplishes nothing and it is free to optimize out undefined behavior... thus the function needs no code. Similarly,  if the last assert is changed to x != 5, dmd -O doesn't even actually do a comparison (the value 5 never appears in the generated code!), it just outputs the direct call to assertion failure.

Well, it's certainly nice to see some evidence that the compiler really is taking advantage of the guarantees that immutable is supposed to provide.

- Jonathan M Davis

January 05, 2018
On Friday, 5 January 2018 at 04:10:54 UTC, Steven Schveighoffer wrote:
>
> Yes, this is undefined behavior.
>
> https://dlang.org/spec/const3.html#removing_with_cast
>
> The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).
>
> The compiler would likely be free to assume *p_x == 5 forever also, if it was clever enough.
>
> I'd recommend not doing this.
>
> -Steve

I should have seen that. Thanks. That makes perfect sense.
January 05, 2018
On Friday, 5 January 2018 at 04:10:54 UTC, Steven Schveighoffer wrote:
>
> Yes, this is undefined behavior.
>
> https://dlang.org/spec/const3.html#removing_with_cast
>
> The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).
>
> The compiler would likely be free to assume *p_x == 5 forever also, if it was clever enough.
>
> I'd recommend not doing this.
>
> -Steve

I also checked that if you create an instance of a class on the heap with an immutable constructor, then it's no longer in the register. Thus, I can now modify the immutable object from the pointer that I casted away immutable (though not that I would!)
January 05, 2018
On Fri, Jan 05, 2018 at 05:50:34PM +0000, jmh530 via Digitalmars-d-learn wrote:
> On Friday, 5 January 2018 at 04:10:54 UTC, Steven Schveighoffer wrote:
> > 
> > Yes, this is undefined behavior.
> > 
> > https://dlang.org/spec/const3.html#removing_with_cast
> > 
> > The compiler assumes x is going to be 5 forever, so instead of loading the value at that address, it just loads 5 into a register (or maybe it just folds x == 5 into true).
> > 
> > The compiler would likely be free to assume *p_x == 5 forever also, if it was clever enough.
> > 
> > I'd recommend not doing this.
> > 
> > -Steve
> 
> I also checked that if you create an instance of a class on the heap with an immutable constructor, then it's no longer in the register. Thus, I can now modify the immutable object from the pointer that I casted away immutable (though not that I would!)

Be careful with that:

	class C { int x; }
	immutable C c = new C(5);
	auto i = c.x;

	C y = cast(C) c;
	y.x = 10;
	i = c.x; // <-- compiler may assume c.x is still 5

Since c.x is read from an immutable object, the compiler may assume that its value hasn't changed the second time you access it, so it may just elide the second assignment to i completely, thereby introducing a bug into the code.

Basically, casting away immutable is UB, and playing with UB is playing with fire. :-P


T

-- 
Береги платье снову, а здоровье смолоду.
January 05, 2018
On Friday, 5 January 2018 at 18:13:11 UTC, H. S. Teoh wrote:
> On Fri, Jan 05, 2018 at 05:50:34PM +0000, jmh530 via Digitalmars-d-learn wrote:
>
> Be careful with that:
>
> 	class C { int x; }
> 	immutable C c = new C(5);
> 	auto i = c.x;
>
> 	C y = cast(C) c;
> 	y.x = 10;
> 	i = c.x; // <-- compiler may assume c.x is still 5
>
> Since c.x is read from an immutable object, the compiler may assume that its value hasn't changed the second time you access it, so it may just elide the second assignment to i completely, thereby introducing a bug into the code.
>
> Basically, casting away immutable is UB, and playing with UB is playing with fire. :-P
>

And these things are nasty. We had one in our C project last month that had us tear our hair out. It was in the end a documentation problem of gcc that induced  the misunderstanding of the purpose of __attribut__((malloc)) and its effect on aliased pointer.