logical const is a subset of transitive const

Steven Schveighoffer schveiguy at yahoo.com
Fri Sep 14 21:00:27 PDT 2007


"Bill Baxter" wrote
> Steven Schveighoffer wrote:
>>
>> IAANACW (I am also not a compiler writer).
>>
>> You are wrong in your assumption.  Even in a single thread, I can 
>> circumvent the constness by having a global non-const pointer to my 
>> object, and using that object.  Very bad implementation, but it would not 
>> break the rules. For example:
>>
>> X mutableinstance;
>>
>> class X
>> {
>>    int bar;
>> }
>>
>> func(const X myref)
>> {
>>   mutableinstance.bar++; // I can do this because I'm not changing myref
>> }
>>
>> myfoolishfunc()
>> {
>>   X foo = new X;
>>   mutableinstance = foo;
>>   foo.bar = 10;
>>   func(foo); // increments foo.bar because mutableinstance is foo
>>
>>   int biff = foo.bar; // biff now should be 11.
>> }
>
> Yeh, ok.  It's perverse, but it does show clearly that even transitive 
> const is not enough to guarantee anything.   But that example is 
> definitely teetering on the edge of undefined behavior.  It depends on 
> whether you take "const X myref" to mean "this function will not modify 
> the contents of myref" vs "this function will not modify myref via the 
> myref handle".   It might be reasonable to say that if you alias a const 
> argument then you're in undefined behavior territory.

My example is perfectly defined and legal, and should behave the same way 
every time.  But it's not likely to be coded this way :)  This kind of thing 
CAN happen in real code, it's just usually that your function calls a global 
function, which calls another function, which adds an object to a queue, 
which uses some mutable reference to your object, and bingo (or even more 
crazy stuff).  The point is that you cannot guarantee thread safety in the 
language without proper thread tools such as mutexes.  Const/invariant is 
not thread safety.

>
>> (BTW you cannot assume the register is still valid after calling func 
>> because func is allowed to change registers, even if it is pure).
>
> Well, if so, that rather pokes a hole in my example.  So I have no clue 
> what great optimizations Walter has in mind.  He should just tell us.

This is just basic assembly.  Every function is free to use registers in the 
processor.  There are clearly defined rules as to what registers mean what 
when calling a function and when returning (this is why you have all those 
stupid modifiers for Microsoft Windows, like STDCALL, and CDECL, these are 
defining different ways functions are called).  All the other registers are 
fair game.  If the compiler wants to save those registers, it must push them 
on the stack, then pop them after the return (which it could do as an 
optimization, I suppose, it depends on what's faster, loading a constant 
into a register, or pushing/popping a value from the stack).

That doesn't mean that optimizations can't be made by the compiler.  In some 
cases you need to declare variables volatile (does D have this concept?) to 
prevent the compiler from optimizing out 2 different references to the 
variable, like so:

if(x == 5)
{
    y = 2;
    if(x == 5 && n == 3) // x == 5 could be optimized out by the compiler 
unless x is volatile.

>
>> However, in multithreaded land, it is questionable whether you can assume 
>> that foo will not change even with pure functions because another thread 
>> could conceivably come along and wreck foo while you are calling your 
>> pure function.
>
> Isn't that "you're on your own" territory there?  Sure some other thread 
> could be changing foo, but if you're using that sort of threading then it 
> was up to you to properly mutex protect foo.  I think the purpose of pure 
> is more to support constructs with more limited scope, like parallel 
> foreach loops.

The purpose of pure, if I understand it correctly is to save time calling 
functions that will return exactly the same value and have no impact on 
other data.  For example, if I have a function f() which is pure, it will 
return the same value each time, so a statement like f() * f() will only 
call f() once.  The compiler could also call execute two different pure 
functions at once through 2 cores without fear that one would interfere with 
the other.

But pure does not guarantee that another thread can't change any state in 
your program.  That is what mutex locks are for.

-Steve 





More information about the Digitalmars-d mailing list