Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Undead objects ([basic.life]/8): why is reference rebinding (and const modification) allowed?

The "undead" clause

I call the undead clause the C++ rule that after the destruction of an object, if a new object is created at the same address, it can sometimes be considered the same object as the old one. That rule always existed in C++ but with some changes on the additional conditions.

I was made to read the latest undead clause by this question. The revised conditions in Lifetime [basic.life]/8 are:

(8.1) the storage for the new object exactly overlays the storage location which the original object occupied, and

Well, duh. An object at a different address would not be the same object.

(8.2) the new object is of the same type as the original object (ignoring the top-level cv-qualifiers), and

Again, duh.

(8.4) neither the original object nor the new object is a potentially-overlapping subobject ([intro.object]).

It cannot a base class, classic (or a member with a special declaration that makes its address not unique). Again, duh.

(8.3) the original object is neither a complete object that is const-qualified nor a subobject of such an object, and

Now that's interesting. The object being replaced can't be either:

  • a complete const object
  • part of a complete const object

On the other hand, the object being resurrected can be:

  1. a const member subobject
  2. a subobject of such const member
  3. an element in an array of const objects

Const subobject

So it seems to me that all of these objects x can be resurrected:

Const member subobject

struct CI {
  const int x;
};

CI s = { 1 };
new ((void*)&s.x) int(2);
int r = s.x; // OK, 2

Subobject of const member:

struct T {
  int x;
};

struct CT {
  const T m = { 1 };
};

CT s;
new ((void*)&s.m.x) int (2);
int r = s.m.x;

Element in an array of const objects:

const int x[1] = { 1 };
new ((void*)&x[0]) int (2);
int r = x[0];

Classes with const and reference members

Also object of class type with const or references members do not seem to be prohibited; the resurrected object is still called x.

Class with a const member:

struct CIM {
  CIM(int i): m(i) {}
  const int m;
};

CIM x(1);
new ((void*)&x) CIM(2);
int r = x.m; // OK, 2

Class with a reference member:

struct CRM {
  CRM (int &r): m(r) {}
  int &m;
};

int i=1,j=2;
CRM x(i);
new ((void*)&x) CRM(j);
int r = x.m; // OK, 2

The questions

  1. Is that interpretation of the clause correct?
  2. If so, is there any other clause that forbid these overwriting operations?
  3. If so, is that intended? Why was that changed?
  4. Is that a breaking change for code generators? Do all compilers really support that? Don't they optimize based on const members, const elements of arrays being immutable and references not being reboundable?
  5. BONUS QUESTION: does that affect ROM-ability of const objects with adequate storage class (not dynamically created objects, of course) and adequate initialize?

Note: I added the bonus later because putting constants in ROM came up in the discussion.

like image 748
curiousguy Avatar asked Dec 12 '19 06:12

curiousguy


1 Answers

It would be surprising if all requirement of the standard related to object life-time were not in [basic-life].

There are few chances that the "complete" adjective has been inadvertedly added to the name "object" in the standard paragraph you cite.

In the paper P0137, one can read this rational (paper cited in @LanguageLawyer comment below):

This is necessary to allow types such as std::optional to contain const subobjects; the existing restriction exists to allow ROMability, and so only affects complete objects.

To reassure us, we can verify that compilers do follow the standard wording at the letter: they perform constant optimization for complete const objects but not for const member suboject of non const complete objects:

Let's consider this code:

struct A{const int m;};

void f(const int& a);

auto g(){
    const int x=12;
    f(x);
    return x;
}

auto h(){
    A a{12};
    f(a.m);
    return a.m;
}

Both Clang and GCC generates this assembly when targeting x86_64:

g():                                  # @g()
        push    rax
        mov     dword ptr [rsp + 4], 12
        lea     rdi, [rsp + 4]
        call    f(int const&)
        mov     eax, 12     ;//the return cannot be anything else than 12
        pop     rcx
        ret
h():                                  # @h()
        push    rax
        mov     dword ptr [rsp], 12
        mov     rdi, rsp
        call    f(int const&)
        mov     eax, dword ptr [rsp]  //the content of a.m is returned
        pop     rcx
        ret

The returned value is placed in register eax (according to the ABI specification: System V x86 processor specific ABI):

  • In the function g the compiler is free to suppose that x can not be changed accross the call to f because x is a complete const object. So the value 12 is placed directly in the eax register as an immediate value: mov eax, 12.

  • In the function h the compiler is not free to suppose that a.m can not be changed accross the call to f because a.m is not a suboject of a complete const object. So after the call to f the value of a.m must be loaded from memory to eax : mov eax, dword ptr [rsp].

like image 155
Oliv Avatar answered Dec 20 '22 07:12

Oliv