4

I was looking to re-use allocated space within the base class from this pointer and C++ Standard does not approve. However, the wording of the standard seems to be wrong. It puts a condition "and before the storage which the object occupied is reused or released", but it is clearly reused in their own code snippet. Where I am getting it wrong?

void B::mutate() {
  new (this) D2;    // reuses storage — ends the lifetime of *this!! REUSED AS WELL SO CONDITION SO RESTRICTIONS DON'T HOLD ANYMORE!
  f();              // undefined behavior

Before the lifetime of an object has started but after the storage which the object will occupy has been allocated41 or, after the lifetime of an object has ended and before the storage which the object occupied is reused or released, any pointer that represents the address of the storage location where the object will be or was located may be used but only in limited ways. For an object under construction or destruction, see [class.cdtor]. Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined. Indirection through such a pointer is permitted but the resulting lvalue may only be used in limited ways, as described below. The program has undefined behavior if:

(6.1) the object will be or was of a class type with a non-trivial destructor and the pointer is used as the operand of a delete-expression,

(6.2) the pointer is used to access a non-static data member or call a non-static member function of the object, or

(6.3) the pointer is implicitly converted ([conv.ptr]) to a pointer to a virtual base class, or

(6.4) the pointer is used as the operand of a static_­cast, except when the conversion is to pointer to cv void, or to pointer to cv void and subsequently to pointer to cv char, cv unsigned char, or cv std​::​byte ([cstddef.syn]), or

(6.5) the pointer is used as the operand of a dynamic_­cast.

[ Example:

    #include <cstdlib>
    struct B {
      virtual void f();
      void mutate();
      virtual ~B();
    };

    struct D1 : B { void f(); };
    struct D2 : B { void f(); };

   /* RELEVANT PART STARTS */ void B::mutate() {
      new (this) D2;    // reuses storage — ends the lifetime of *this
      f();              // undefined behavior 
      /* RELEVANT PART ENDS */
      ... = this;       // OK, this points to valid memory
    }

   void g() {
      void* p = std::malloc(sizeof(D1) + sizeof(D2));
      B* pb = new (p) D1;
      pb->mutate();
      *pb;              // OK: pb points to valid memory
      void* q = pb;     // OK: pb points to valid memory
      pb->f();          // undefined behavior, lifetime of *pb has ended
    }
curiousguy
  • 8,038
  • 2
  • 40
  • 58
minex
  • 329
  • 1
  • 5
  • Please check the formatting of your question. It's not clear to me where the question text ends and where the code is supposed to start. – François Andrieux Jul 10 '19 at 18:59
  • It's unclear to me what you are asking. What part of the code do you think be invalid but the standard says it's okay? – NathanOliver Jul 10 '19 at 19:00
  • There are cases where you can placement `new` over a live object but, if I remember correctly, none of those cases apply to polymorphic objects. – François Andrieux Jul 10 '19 at 19:01
  • **void B::mutate() { new (this) D2; // reuses storage — ends the lifetime of *this f(); // undefined behavior** ... = this; // OK, this points to valid memory } it says f() is undefined behaviour, but it shall not because the condition for the rule does not hold in the first place! Since D2 ends the liftime of original object and starts the lifetime of the new object! Hence using pointer to this shall be well defined – minex Jul 10 '19 at 19:03
  • 1
    `f()` is using `*this` but it can't because `*this` was destroyed. – NathanOliver Jul 10 '19 at 19:07
  • @NathanOliver. But didn't this code below ended the lifetime of the original object and then started the lifetime of the new object. Standard says this cannot be used if this is not reused, but it clears is after placement new --->>> new (this) D2; // reuses storage — ends the lifetime of *this – minex Jul 10 '19 at 19:10
  • @minex I'm writing up an answer right now to clear up you confusion. – NathanOliver Jul 10 '19 at 19:11
  • @FrançoisAndrieux The ability to use placement new has nothing to do with polymorphism. – curiousguy Jul 26 '19 at 02:22
  • @curiousguy I said that there are cases where you cannot placement `new` *over an object that is still within it's lifetime* and that one of those cases is if the object's type is polymorphic. You must call the destructor explicitly before you do that, unless the desctructor is trivial. Though now I realize that a polymorphic type doesn't strictly require that the base class' destructor be `virtual`, so a polymorphic object *might* sometimes be trivial. – François Andrieux Jul 26 '19 at 13:55
  • @FrançoisAndrieux Consider the case of pure memory resources (no resources other than dynamic allocation inside the process space) and process termination. Do you claim it's a bug to not cleanup at process termination? Now consider allocation inside an "arena" that is going away. Would you want to restore the arena to its empty state? That would seem silly and wasteful. Why would anyone care about properly calling dtors in general? – curiousguy Jul 26 '19 at 17:38
  • @FrançoisAndrieux IOW, what makes you believe calling a dtor has any real world meaning if the object is being erased anyway? Except for a sense of symmetry between construction and destruction. Calling useless dtors has never been required. Hopefully! – curiousguy Jul 26 '19 at 17:50
  • @curiousguy To the first reply : that has nothing to do with what I said. This shifts the discussion to an entirely new place. I in no way made the claim you ask about in that reply. To the second reply : I said the opposite, you **don't** need call trivial destructors. – François Andrieux Jul 26 '19 at 18:08
  • @FrançoisAndrieux In complex C++ code, many objects have non trivial dtors but they don't manage resources other than memory. Do these dtors need to be called? – curiousguy Jul 26 '19 at 18:35
  • @curiousguy It seems, once again, you are baiting me into an argument that's not related to the initial topic. I'm afraid, this time, I must decline. – François Andrieux Jul 26 '19 at 18:38
  • @FrançoisAndrieux You are wrong, period. There is no reason to call a dtor if you don't need to do the cleanup. – curiousguy Jul 26 '19 at 18:39
  • @curiousguy That's what I said... **" you don't need call trivial destructors"**. Edit : looks like I missed a "to" in "don't need to call". – François Andrieux Jul 26 '19 at 18:41
  • @FrançoisAndrieux No it isn't what you said at all. Please read my comments again. You only need to call a dtor if you depend on the cleanup it would do if called. Like a function called `cleanup_stuff`. – curiousguy Jul 26 '19 at 18:45

2 Answers2

3

When you do

f();

in a member function what you are really doing is

this->f();

So in the example when they do

new (this) D2; 

it ends the lifetime of the thing pointer to by this and creates a new D2 in it's place. That makes you think that this->f(); is okay since this now points to an object that has had it's lifetime started but you are forgetting that this is a pointer to an object that has had it's lifetime ended. You can't use it to refer to the new object that you made.

In order to be able to call f() legally what you would need to do is capture the pointer returned by new and use it to access the new object

void B::mutate() {
  auto np = new (this) D2;    // reuses storage — ends the lifetime of *this
  f();              // undefined behavior**
  np->f()           // OK, np points to a valid object
  ... = this;       // OK, this points to valid memory
}
NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • If `auto x = this;` is legal after the placement new, is dereferencing `x` legal? – François Andrieux Jul 10 '19 at 19:22
  • 1
    @FrançoisAndrieux No. `this` can't be used anymore except to get its value. Treat placement new as making the pointer invalid for access use. – NathanOliver Jul 10 '19 at 19:23
  • @FrançoisAndrieux What you can do though is use [`std::launder`](https://en.cppreference.com/w/cpp/utility/launder) with `this` to call `f()` like `std::launder(this)->f();` – NathanOliver Jul 10 '19 at 19:26
  • 1
    @FrançoisAndrieux Well, dereferencing i.e. indirecting is permitted, as per the rule quoted in the question, *but the resulting lvalue may only be used in limited ways, as described [further in that rule]*. – eerorika Jul 10 '19 at 19:26
  • @NathanOliver. According to the standart, this pointer can still be used if roughly speaking this all happens to the same class https://timsong-cpp.github.io/cppwp/n4659/basic.life#8 – minex Jul 10 '19 at 19:34
  • @minex The pointers aren't the same type though. `this` pointed to a `D1`, and now there is a `D2` there. – NathanOliver Jul 10 '19 at 19:39
  • @NathanOliver. That is right, just a further exploration. How tricky things are in C++! – minex Jul 10 '19 at 19:40
  • C++ offers many ways to shot ones' self in the foot. It makes life exciting that way ;) – NathanOliver Jul 10 '19 at 19:42
  • If you take the literal meaning of the std, pointers are trivial types, which means `std::launder` is the identity function: it's never needed! – curiousguy Jul 26 '19 at 02:30
  • @curiousguy It's not because it's an identity function that it doesn't change how the compiler behaves. Look at `reinterpret_cast`. – François Andrieux Jul 26 '19 at 18:09
  • @FrançoisAndrieux Well officially `reinterpret_cast` doesn't change "how the compile behaves" (regarding pointers to objects); you can only use it to store a value in another type and convert back. Anyway the std text about pointers fails completely to describe the inherent C++ language that ppl intuitively understand. That makes asking language lawyer question quite silly because the correct answer is often "disregard the std text" which isn't considered acceptable here (SO doesn't tolerate different viewpoint). – curiousguy Jul 26 '19 at 18:38
1

but it is clearly reused in their own code snippet.

new (this) D2;    // reuses storage — ends the lifetime of *this
f();              // undefined behavior**
... = this;       // OK, this points to valid memory

Correct. Because the stoarge has been reused, the "Otherwise" clause applies:

... Otherwise, such a pointer refers to allocated storage ([basic.stc.dynamic.deallocation]), and using the pointer as if the pointer were of type void*, is well-defined.

Calling f() through a void* is not possible, so it is not allowed by that clause. Otherwise, calling member functions of an object whose lifetime has ended is undefined (outside of the destructor).

... = this; on the other hand is something that can be done with a void*.

Note that (new (this) D2)->f() would be well-defined.

Community
  • 1
  • 1
eerorika
  • 232,697
  • 12
  • 197
  • 326
  • That "Otherwise" indeed made my mental model work :) Thanks a lot! – minex Jul 10 '19 at 19:16
  • @minex I believe this "otherwise"-clause is to be understood as an else clause only to the preceding sentence which mentions "objects under construction or destruction". This is apparently how it was added: https://cplusplus.github.io/CWG/issues/793.html – JMC Aug 30 '23 at 16:58
  • @JMC but then my original question is reopened :) Or the whole point is that because it was reused, it is still undefined *but* for another reason like D2 is not transparently replaceable with B, which just makes their example confusing. – minex Sep 02 '23 at 18:54