6

Is it possible to use placement-new to change the type of a polymorphic object? if not, what exactly in the standard prohibits it?

Consider this code:

#include <new>

struct Animal {
    Animal();
    virtual ~Animal();
    virtual void breathe();
    void kill();
    void *data;
};

struct Dead: Animal {
    void breathe() override;
};

void Animal::kill() {
    this->~Animal();
    new(this) Dead;
}

Is calling "kill" ever legal?

Update: Early comments do not address the (il)legality according to the standard of the programming technique shown here of changing the type of an object by explicit call to destructor and applying placement-new for a new object compatible.

Because there is interest in why would anyone would like to do this, I can mention the use case that led me to the question, although it is not relevant to the question I am asking.

Imagine you have a polymorphic type hierarchy with several virtual methods. During the lifetime of the object, things happen that can be modeled in the code as the object changing its type. There are many perfectly legal ways to program this, for example, keeping the object as a pointer, smart or not, and swapping in a copy of the other type. But this may be expensive: One has to clone or move the original object into another one of a different type just to swap the new in.

In GCC, Clang, and others, changing the type of an object can be as cheap as to simply changing the virtual table pointer, but in portable C++ this is not possible except by constructing in-place an object of a new type.

In my original use case, the object could not be held as a pointer either.

I would like to know what the standard says on the subject of reusing memory.

TheCppZoo
  • 1,219
  • 7
  • 12
  • 3
    Why would you ever want to do this? – Justin Sep 11 '17 at 22:04
  • 1
    I don't know if "Dead is an Animal" conceptually makes much sense. Why create a new class just to represent a state change? – Carcigenicate Sep 11 '17 at 22:07
  • 1
    You're better off either marking the `Animal` as dead with a member variable, or removing it from whatever container is resides in and `delete`ing it normally. You almost never have to manually call the destructor. – user4581301 Sep 11 '17 at 22:19
  • Generally you can't use placement-new to change the type of a polymorphic object, in case the two are different sizes. – jcarpenter2 Sep 11 '17 at 22:23
  • 1
    This is viable, if weird, but presents problems if `Dead` requires more storage than was allocated for the `Amimal`. Need more data to tell if this is legal. legal's a bad word. Safe is probably better. – user4581301 Sep 11 '17 at 22:23
  • @Justin : This is practically motivated. I have a design that tries to keep the things in the same memory space, and whenever things happen, the behavior changes by changing the type of the object itself. I practically do that here: https://github.com/thecppzoo/zoo/blob/cfabf0c714d9e97f1d2a87d9c176c61d3950f2cb/inc/util/any.h#L105 – TheCppZoo Sep 11 '17 at 22:27
  • @Carcigenicate changing the type of the object would automatically change the behavior of all of the overriden methods, it is the cheapest way to accomplish that change. But is it legal? – TheCppZoo Sep 11 '17 at 22:28
  • @EdMaster It seems like it would be a better design for `Animal` to not care about where it is at, but to have some wrapper that ensures it stays in the same memory location by managing the object completely (placement new, destructors, everything; the wrapper would handle the lifetime). Alternatively, for trying to keep things in the same memory space, a custom allocator may be the way to go. – Justin Sep 11 '17 at 22:29
  • @Justin All the choices you mention require work, and may have additional performance costs. My question is why is that simple code not legal, since it would simplify. – TheCppZoo Sep 11 '17 at 22:33
  • In theory you could replace pointer to `vtable` in the object but there is no simple way to do this. This could interfere with compiler optimizations. Maybe you can avoid it by some tricks but there can be other problems. I would say it is doable but barely. Perhaps, it would be better idea to implement something like second vtable with pointers to all methods that would differ for different states of `Animal`. – Piotr Siupa Sep 12 '17 at 00:29
  • @NO_NAME there is no portable way to replace the VTable pointer, since C++ does not specify that runtime dispatch is implemented with VTable, in practice, it is, and I think changing it through placement new may be valid. If it is a valid programming technique, and compilers don't support it, it is their bug. Hence the importance of knowing whether it is valid according to the standard – TheCppZoo Sep 12 '17 at 03:40
  • If you'd store your objects as `std::unique_ptr` you just reset it to the Dead and store instead - no problem replacing one object with another – Artemy Vysotsky Sep 12 '17 at 04:10
  • @ArtemyVysotsky this is a question about the rules of he language, not a discussion of the different ways to accomplish the same thing – TheCppZoo Sep 12 '17 at 04:18
  • @EdMaster - then change the title or your question. Since you asked about portable way to replace the polymorphic object. And this is what I answered to. – Artemy Vysotsky Sep 12 '17 at 04:21
  • @ArtemyVysotsky I changed the question as you suggested, however, your comment did not address the original question either, because resetting the unique_ptr does not change the type of any object, just makes it point to a different one, as you say in your comment, you are "replacing one object with another", not changing the type of an object or its memory – TheCppZoo Sep 12 '17 at 04:35
  • Provided two objects are of the same size, and allocated on the heap, I don't see UB here. – n. m. could be an AI Sep 12 '17 at 05:09
  • I don't see UB here. But uses of the object are very prone to UB. See [\[basic.life\]/6](http://eel.is/c++draft/basic.life#6). – cpplearner Sep 12 '17 at 13:14
  • The standard [has an example](http://eel.is/c++draft/basic.life#6.example-1) that's almost exactly like yours, and claims it's UB. The call to `kill()` itself is valid, but most attempts to use `Animal*` pointer through which `kill()` was called would trigger UB by way of accessing an object whose lifetime has ended. – Igor Tandetnik Sep 12 '17 at 13:45
  • Then there's [\[basic.life\]/8](http://eel.is/c++draft/basic.life#8) which says it's OK to destroy the object and re-create another object of the same type in its place - but only if both objects are most-derived objects of their type. In your example, you replace a most-derived object with base class subobject. – Igor Tandetnik Sep 12 '17 at 13:53

1 Answers1

2

[basic.life]/8 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:

...

(8.4) — 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).

In your example, a call to kill() itself may be valid (if it so happens that sizeof(Animal)==sizeof(Dead), which I don't think is guaranteed), but most attempts to use Animal* pointer or Animal& lvalue through which that call was made would trigger undefined behavior by way of accessing the object whose lifetime has ended. Even assuming that the stars align and Animal subobject of Dead perfectly overlays the original location of the original stand-alone Animal object, such a pointer or lvalue is not considered to refer to the former, but to the now-expired latter.

Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • Thanks, Igor. It settles the question. Yes, the caller of kill knows it does not have a valid reference/pointer to the old animal anymore. In my use case these are raw bytes, with a reinterpret cast to get the equivalent of an Animal, the code respects strict aliasing since no two pointers are ever used looking at the same memory as two different types. This is the exact line that led to the question: https://github.com/thecppzoo/zoo/blob/cfabf0c714d9e97f1d2a87d9c176c61d3950f2cb/inc/util/any.h#L105 Once a driver moves the pointer it controls it reclassifies itself as holding no object – TheCppZoo Sep 12 '17 at 15:43
  • Note the quoted text (8.4) changed with [P0840R2](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p0840r2.html). It [now says](http://eel.is/c++draft/basic.life#8.4) "neither the original object nor the new object is a potentially-overlapping subobject". – Lack Oct 14 '18 at 16:56
  • Would it be legal to return `std::launder(reinterpret_cast(this))` from `kill` though? – Aardappel Jan 05 '19 at 00:55
  • @Aardappel Perhaps (I'm too lazy to dig into it), but that would kinda defeat the point. Far as I can tell, the whole point of `kill` method (as written, returning `void`) is to preserve existing `Animal*` pointers and `Animal&` references, but have them magically refer to a `Dead` object and start exhibiting different polymorphic behavior. If you are willing to have the caller reassign the pointer, you may just as well write `Dead* kill() { delete this; return new Dead; }` – Igor Tandetnik Jan 05 '19 at 01:27