In Visual C++ 2017, when experimenting with what happens when you break the rules, I found that if I cast a const int to an int *, and then reassign a value to the int *, the debugger will change the value of the const, but the runtime execution won't.

This happens whether or not I run it in Debug mode or as a released executable. I'm aware it's undefined, but am looking for insight as to where these values are held, as they appear to be identical locations.

const int j = 100;
//int *q = &j; //Compiler disallows
int *q = (int*)&j; //By some magic, now allowed
*q = 300; //After this line, j = 300 in debugger
cout << "j = " << j << endl; //300 in debugger, 100 in console
//^ What is happening here? Where are the two values stored?
cout << "*q = " << *q << endl; //300 in both

//Output:
//  j = 100
//  *q = 300

Where are the two values being stored? This is like having one bucket that is simultaneously filled with two different liquids.

I'm aware that it's Undefined Behavior, but I was wondering if anyone could shed light on what is happening, internally.

3

There are 3 best solutions below

0
On

If an object is in const storage, a compiler may at its leisure replace it with two or more objects that have the same content if it can tell that the addresses are never compared. A compiler would not generally be able to do this if both objects' addresses get exposed to the outside world, but may do so in cases where one object is exposed but the other(s) are not.

Consider, for example:

const char Hey[4] = "Hey";

void test(int index)
{
  char const *HeyPtr = Hey;
  putchar(HeyPtr[index]);
}

A compiler processing test would be able to see that the value of HeyPtr is never exposed to outside code in any way, and on some platforms might benefit from having the test function use its own copy of the string. On a platform where addresses are 64 bits, if test doesn't include its own copy of the string, then eight bytes would needed to contain the address of Hey. The four bytes needed to store an extra copy of the string would cost less than the eight bytes needed to hold the address.

There are a few situations where the Standard offers guarantees that are stronger than programmers generally need. For example, given:

const int foo[] = {1,2,3,4};
const int bar[] = {1,2,3,4};

Unless a program happens to compare foo (or an address derived from it) with bar (likewise), using the same storage for both objects would save 16 bytes without affecting program semantics. The Standard, however, provides no means by which a programmer could indicate that code either won't compare those addresses, or would not be adversely affected if they happen to compare equal, so a compiler can only make such substitutions in cases where it can tell that a substituted object's address won't be exposed to code that might perform such comparisons.

2
On

The premise is flawed. The debugger works by the same C++17 rules, so it too can assume that there is no Undefined Behavior. That means it can check the source code and know j==100. There's no reason it would have to check the runtime value.

0
On

Well just look at the generated assembly...

    const int j = 100;
00052F50  mov         dword ptr [j],64h  
    //int *q = &j; //Compiler disallows
    int *q = (int*)&j; //By some magic, now allowed
00052F58  lea         rax,[j]  
00052F5D  mov         qword ptr [q],rax  
    *q = 300; //After this line, j = 300 in debugger
00052F62  mov         rax,qword ptr [q]  
00052F67  mov         dword ptr [rax],12Ch  
    cout << "j = " << j << endl; //300 in debugger, 100 in console
00052F6D  lea         rdx,[__xt_z+114h (07FF679CC6544h)]  
00052F74  lea         rcx,[std::cout (07FF679D31B80h)]  
00052F7B  call        std::operator<<<std::char_traits<char> > (07FF679B43044h)  
00052F80  mov         edx,64h  
00052F85  mov         rcx,rax  
00052F88  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B417E9h)  
00052F8D  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF679B42C25h)]  
00052F94  mov         rcx,rax  
00052F97  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B445F7h)  
    //^ What is happening here? Where are the two values stored?
    cout << "*q = " << *q << endl; //300 in both
00052F9C  lea         rdx,[__xt_z+11Ch (07FF679CC654Ch)]  
00052FA3  lea         rcx,[std::cout (07FF679D31B80h)]  
00052FAA  call        std::operator<<<std::char_traits<char> > (07FF679B43044h)  
00052FAF  mov         rcx,qword ptr [q]  
00052FB4  mov         edx,dword ptr [rcx]  
00052FB6  mov         rcx,rax  
00052FB9  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B417E9h)  
00052FBE  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF679B42C25h)]  
00052FC5  mov         rcx,rax  
00052FC8  call        std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF679B445F7h)  

Notice the "weird" read from __xt_z+114h. That's an offset from the end of global initializers (__xt_z is probably the nearest symbol that the debugger found), most probably into the read-only data section (.rdata).

That's where the Debug version puts the 100 (it's a constant after all).

Then, an MSVC Debug version always allocates local variables and constants on the stack, hence you get a separate j variable, which you can even modify (Note the compiler does not have to read from it when you read j, since it knows j is a constant that contains 100).

If we try the same in Release mode, we see the compiler did value propagation and optimized away both variables, simply inlining the values into the code:

    const int j = 100;
    //int *q = &j; //Compiler disallows
    int *q = (int*)&j; //By some magic, now allowed
    *q = 300; //After this line, j = 300 in debugger
    cout << "j = " << j << endl; //300 in debugger, 100 in console
000C101D  lea         rdx,[string "j = " (07FF72FAC3298h)]  
000C1024  mov         rcx,qword ptr [__imp_std::cout (07FF72FAC30A8h)]  
000C102B  call        std::operator<<<std::char_traits<char> > (07FF72FAC1110h)  
000C1030  mov         edx,64h  
000C1035  mov         rcx,rax  
000C1038  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC30A0h)]  
000C103E  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72FAC12E0h)]  
000C1045  mov         rcx,rax  
000C1048  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC3098h)]  
    //^ What is happening here? Where are the two values stored?
    cout << "*q = " << *q << endl; //300 in both
000C104E  lea         rdx,[string "*q = " (07FF72FAC32A0h)]  
000C1055  mov         rcx,qword ptr [__imp_std::cout (07FF72FAC30A8h)]  
000C105C  call        std::operator<<<std::char_traits<char> > (07FF72FAC1110h)  
000C1061  mov         edx,12Ch  
000C1066  mov         rcx,rax  
000C1069  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC30A0h)]  
000C106F  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72FAC12E0h)]  
000C1076  mov         rcx,rax  
000C1079  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72FAC3098h)]  

In both cases the output is the same. A const variable remains unchanged.

Does any of it matter? No, you shouldn't rely on this behavior and you shouldn't modify constants.