I have an object which I want to 'transform' into another object. For this I am using a placement new
on the first object which creates a new object of the other type on top of its own address.
Consider the following code:
#include <string>
#include <iostream>
class Animal {
public:
virtual void voice() = 0;
virtual void transform(void *animal) = 0;
virtual ~Animal() = default;;
};
class Cat : public Animal {
public:
std::string name = "CAT";
void voice() override {
std::cout << "MEOW I am a " << name << std::endl;
}
void transform(void *animal) override {
}
};
class Dog : public Animal {
public:
std::string name = "DOG";
void voice() override {
std::cout << "WOOF I am a " << name << std::endl;
}
void transform(void *animal) override {
new(animal) Cat();
}
};
You can see that when a Dog
is called with transform
it creates a new Cat
on top of the given address.
Next, I will call the Dog::transform
with its own address:
#include <iostream>
#include "Animals.h"
int main() {
Cat cat{};
Dog dog{};
std::cout << "Cat says: ";
cat.voice() ;
std::cout << "Dog says: ";
dog.voice();
dog.transform(&dog);
std::cout << "Dog says: ";
dog.voice();
std::cout << "Dog address says: ";
(&dog)->voice();
return 0;
}
The results of this is:
Cat says: MEOW I am a CAT
Dog says: WOOF I am a DOG
Dog says: WOOF I am a CAT
Dog address says: MEOW I am a CAT
My questions are:
dog.voice()
. It correctly prints the name CAT
(it is now a cat), but still writes WOOF I am a
, even though I would have thought that it should call the Cat
's voice
method? (You can see is that I call the same method but by the address ((&dog)->voice()
), everything is working properly.Because placement new does not allocate memory, you should not use delete to deallocate objects created with the placement syntax. You can only delete the entire memory pool ( delete whole ). In the example, you can keep the memory buffer but destroy the object stored in it by explicitly calling a destructor.
Placement new allows you to construct an object in memory that's already allocated. You may want to do this for optimization when you need to construct multiple instances of an object, and it is faster not to re-allocate memory each time you need a new instance.
With std::vector , a memory buffer of the appropriate size is allocated without any constructor calls. Then objects are constructed in place inside this buffer using "placement new".
A placement new expression first calls the placement operator new function, then calls the constructor of the object upon the raw storage returned from the allocator function.
Does this operation considered safe, or does it leave the object in unstable state?
This operation is not safe and causes undefined behavior. Cat
and Dog
have non trivial destructors so before you can reuse the storage cat
and dog
have you have to call their destructor so the previous object is cleaned up correctly.
After the transform I call
dog.voice()
. I prints correctly theCAT
name (it is now a cat), but still writesWOOF I am a
, even tough I would have thought that it should call theCat
'svoice
method? (You can see is that I call the same method but by the address ((&dog)->voice()
), everything is working properly.
Using dog.voice();
after dog.transform(&dog);
is undefined behavior. Since you've reused its storage without destroying it, you have undefined behavior. Lets say you do destroy dog
in transform
to get rid of that bit of undefined behavior you still aren't out of the woods. Using dog
after it has been destroyed is undefined behavior. What you would have to do is capture the pointer placement new returns and use that pointer from then on. You could also use std::launder
on dog
with a reinterpret_cast
to the type you transformed it to but it's not worth since you lose all encapsulation.
You also need to make sure when using placement new that the object you are using is large enough for the object you are constructing. In this case it should be since the classes are the same but a static_assert
comparing the sizes will guarantee that and stop the compilation if it is not true.
One way you can fix this is to create a different animal class that acts as a holder of your animal class (I renamed it to Animal_Base
in the sample code below). This lets you encapsulate the changing of what type of object an Animal
represents. Changing your code to
class Animal_Base {
public:
virtual void voice() = 0;
virtual ~Animal_Base() = default;
};
class Cat : public Animal_Base {
public:
std::string name = "CAT";
void voice() override {
std::cout << "MEOW I am a " << name << std::endl;
}
};
class Dog : public Animal_Base {
public:
std::string name = "DOG";
void voice() override {
std::cout << "WOOF I am a " << name << std::endl;
}
};
class Animal
{
std::unique_ptr<Animal_Base> animal;
public:
void voice() { animal->voice(); }
// ask for a T, make sure it is a derived class of Animal_Base, reset pointer to T's type
template<typename T, std::enable_if_t<std::is_base_of_v<Animal_Base, T>, bool> = true>
void transform() { animal = std::make_unique<T>(); }
// Use this to say what type of animal you want it to represent. Doing this instead of making
// Animal a temaplte so you can store Animals in an array
template<typename T, std::enable_if_t<std::is_base_of_v<Animal_Base, T>, bool> = true>
Animal(T&& a) : animal(std::make_unique<T>(std::forward<T>(a))) {}
};
and then adjusting main
to
int main()
{
Animal cat{Cat{}};
Animal dog{Dog{}};
std::cout << "Cat says: ";
cat.voice() ;
std::cout << "Dog says: ";
dog.voice();
dog.transform<Cat>();
std::cout << "Dog says: ";
dog.voice();
std::cout << "Dog address says: ";
(&dog)->voice();
return 0;
}
produces the output
Cat says: MEOW I am a CAT
Dog says: WOOF I am a DOG
Dog says: MEOW I am a CAT
Dog address says: MEOW I am a CAT
and this is safe and portable.
You have at least three issues with this code:
Dog
object after it's storage has been reused.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With