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 theCATname (it is now a cat), but still writesWOOF I am a, even tough I would have thought that it should call theCat'svoicemethod? (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