Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it legal to modify a dynamically-allocated `const` object through a re-used non-`const` name?

Consider the following program:

#include <iostream>

int main()
{
   int x = 0;
   const int* px = new (&x) const int(0);
   x = 1;
   std::cout << *px;  // 1?
}

It compiles under GCC 4.8 (and produces the "expected" output), but I suspect it's entirely UB because the dynamic object has type const int (which remains part of the type). But, then, if so why isn't the compiler stopping me from violating const-correctness?

like image 780
Lightness Races in Orbit Avatar asked Apr 02 '14 10:04

Lightness Races in Orbit


1 Answers

tl;dr: Yes, it's undefined behavior. No, the compiler doesn't diagnose it.


In general, the compiler won't (and sometimes can't) diagnose UB. More obvious examples of const-correctness violation are in fact ill-formed and can be diagnosed:

#include <iostream>

int main()
{
   const int x = 0;
   x = 1;
   std::cout << x;
}

// g++-4.8 -std=c++11 -O2 -Wall -pedantic -pthread main.cpp && ./a.out
// main.cpp: In function 'int main()':
// main.cpp:6:6: error: assignment of read-only variable 'x'
//     x = 1;
//       ^

But, other than that, it won't stop you from performing obvious violations of const-correctness:

#include <iostream>

int main()
{
    const int x = 0;
    *const_cast<int*>(&x) = 1;
    std::cout << x;
}

// Output: 1

So, going back to your code snippet, I wouldn't expect too much in the way of compiler diagnostics there.

Still, your code does invoke undefined behaviour. Let's examine it:

#include <iostream>

int main()
{
   int x = 0;
   const int* px = new (&x) const int(0);
   x = 1;
   std::cout << *px;  // 1?
}

Here's what happens:

  1. An int is created with automatic storage duration, initialised to 0.
  2. The name x refers to this object.
  3. A const int is created with dynamic storage duration, re-using the int's storage.
  4. The int's lifetime ends1, 2.
  5. x now refers to the const int3.
  6. Although the name x has type int, it's now referring to a const int, so the assignment is undefined4.

This is an interesting loophole you can use to "get around" const-correctness and, as long as the original int didn't live in read-only memory, it probably won't even result in a crash.

However, it's still undefined and although I can't see what optimisations may be performed that could break the assignment and subsequent read, you're definitely open to all sorts of unexpected nastiness, such as spontaneous volcanoes in your back garden or all your hard-earned rep being transformed into Pounds Sterling and deposited in my bank account (thanks!).


Footnote 1

[C++11: 3.8/1]: [..] The lifetime of an object of type T ends when:

  • if T is a class type with a non-trivial destructor (12.4), the destructor call starts, or
  • the storage which the object occupies is reused or released.

Footnote 2

Note that I did not have to explicitly call the "destructor" on the int object. This is mostly because such objects do not have a destructor, but even if I'd picked a simple class T rather than int, I may not have needed an explicit destructor call:

[C++11: 3.8/4]: A program may end the lifetime of any object by reusing the storage which the object occupies or by explicitly calling the destructor for an object of a class type with a non-trivial destructor. For an object of a class type with a non-trivial destructor, the program is not required to call the destructor explicitly before the storage which the object occupies is reused or released; however, if there is no explicit call to the destructor or if a delete-expression (5.3.5) is not used to release the storage, the destructor shall not be implicitly called and any program that depends on the side effects produced by the destructor has undefined behavior.

Footnote 3

[C++11: 3.8/7]: If, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, a new object is created at the storage location which the original object occupied, a pointer that pointed to the original object, a reference that referred to the original object, or the name of the original object will automatically refer to the new object and, once the lifetime of the new object has started, can be used to manipulate the new object, if:

  • the storage for the new object exactly overlays the storage location which the original object occupied, and
  • the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and
  • the type of the original object is not const-qualified, and, if a class type, does not contain any non-static data member whose type is const-qualified or a reference type, and
  • the original object was a most derived object (1.8) of type T and the new object is a most derived object of type T (that is, they are not base class subobjects). [..]

Footnote 4

[C++11: 7.1.6.1/4]: Except that any class member declared mutable (7.1.1) can be modified, any attempt to modify a const object during its lifetime (3.8) results in undefined behavior. [..]

(Examples follow that are similar to, but not quite the same as, your code snippet.)

like image 138
Lightness Races in Orbit Avatar answered Oct 04 '22 10:10

Lightness Races in Orbit