15

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:

  1. Is this operation considered safe, or does it leave the object in unstable state?
  2. After the transform I call 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.
John Kugelman
  • 349,597
  • 67
  • 533
  • 578
Guy Yafe
  • 991
  • 1
  • 8
  • 26
  • 8
    I can't cite where in the standard it says that this isn't allowed, but I can say that I get "WOOF I am a CAT" in both of the bottom lines on my system, which is a pretty good indicator that this behavior is not portable. – Silvio Mayolo Apr 16 '19 at 15:07
  • 2
    If you need this behavior which I would describe as "The object will appear to change it's class", consider using the Gang of Four State Pattern: https://en.wikipedia.org/wiki/State_pattern – Reginald Blue Apr 16 '19 at 17:24

3 Answers3

15

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 the CAT name (it is now a cat), but still writes WOOF I am a, even tough 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.

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.

NathanOliver
  • 171,901
  • 28
  • 288
  • 402
  • 1
    Very well said, and +1 for pointing out more esoteric options like `std::launder`. The only thing I would add is a recommendation to use standard polymorphism instead of... whatever you call `transform()`. – Kevin Apr 17 '19 at 04:27
  • @Kevin I was thinking about it an decided to do one better an have added an example of how the OP can get the behavior they are looking for. – NathanOliver Apr 17 '19 at 12:40
6

1) No, this is not safe for the following reasons:

  • The behavior is undefined and can be different for some compilers.
  • The allocated memory needs to be big enough to hold the newly created structure.
  • Some compilers might call the destructor of the original object even if it is virtual, which would lead to leaks and crashes.
  • In your code, the destructor of the original object is not called, so it can lead to memory leaks.

2) I observed on MSVC2015 that dog.voice() will call Dog::voice without checking the actual virtual table. In the second case, it checks the virtual table, which has been modified to be Cat::voice. However, as experienced by other users, some other compiler might perform some optimizations and directly call the method that matches the declaration in all cases.

6

You have at least three issues with this code:

  • There is no guarantee that when placement new is called the size of the object you are constructing your new object in is sufficient to hold the new object
  • You are not calling destructor of the object used as a placeholder
  • You use the Dog object after it's storage has been reused.
SergeyA
  • 61,605
  • 5
  • 78
  • 137