www.digitalmars.com         C & C++   DMDScript  

digitalmars.D.learn - Consequences of casting away immutable from pointers

reply jmh530 <john.michael.hall gmail.com> writes:
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
}
Jan 04 2018
parent reply Steven Schveighoffer <schveiguy yahoo.com> writes:
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
Jan 04 2018
next sibling parent reply Adam D. Ruppe <destructionator gmail.com> writes:
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.
Jan 04 2018
parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
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
Jan 04 2018
prev sibling next sibling parent Jonathan M Davis <newsgroup.d jmdavisprog.com> writes:
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
Jan 04 2018
prev sibling next sibling parent jmh530 <john.michael.hall gmail.com> writes:
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.
Jan 05 2018
prev sibling parent reply jmh530 <john.michael.hall gmail.com> writes:
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!)
Jan 05 2018
parent reply "H. S. Teoh" <hsteoh quickfur.ath.cx> writes:
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 -- Береги платье снову, а здоровье смолоду.
Jan 05 2018
parent Patrick Schluter <Patrick.Schluter bbox.fr> writes:
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.
Jan 05 2018