Переопределение объекта в памяти с размещением нового

У меня есть объект, который я хочу "преобразовать" в другой объект. Для этого я использую placement new на первом объекте, который создает новый объект другого типа поверх его собственного адреса.

Рассмотрим следующий код:

#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();
  }
};

Вы можете видеть, что когда Dog вызывается с помощью transform он создает нового Cat поверх данного адреса.
Далее я вызову Dog::transform со своим собственным адресом:

#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;
}

Результаты этого:

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

Мои вопросы:

  1. Эта операция считается безопасной или она оставляет объект в нестабильном состоянии?
  2. После преобразования я вызываю dog.voice(). Он правильно печатает имя CAT (теперь это кот), но все равно пишет WOOF я am a, хотя я и подумал, что он должен вызывать voice метод Cat? (Вы можете видеть, что я вызываю тот же метод, но по адресу ((&dog)->voice()) все работает правильно.

Ответ 1

Эта операция считается безопасной или она оставляет объект в нестабильном состоянии?

Эта операция небезопасна и вызывает неопределенное поведение. Cat и Dog имеют нетривиальные деструкторы, поэтому, прежде чем вы сможете повторно использовать хранилище, cat и dog должны вызвать их деструктор, чтобы предыдущий объект был правильно очищен.

После преобразования я вызываю dog.voice(). Я правильно CAT имя CAT (теперь это кот), но все равно пишет WOOF я am a, даже если я подумал, что он должен вызывать voice метод Cat? (Вы можете видеть, что я вызываю тот же метод, но по адресу ((&dog)->voice()) все работает правильно.

Использование dog.voice(); после dog.transform(&dog); является неопределенным поведением. Поскольку вы повторно использовали хранилище, не уничтожая его, у вас неопределенное поведение. Допустим, вы уничтожаете dog в transform чтобы избавиться от неопределенного поведения, которое вы до сих пор не покинули. Использование dog после ее уничтожения - неопределенное поведение. Что вам нужно сделать, это захватить указатель размещения новых возвратов и использовать этот указатель с тех пор. Вы также можете использовать std::launder на dog с reinterpret_cast для типа, в который вы его преобразовали, но это не стоит, так как вы теряете всю инкапсуляцию.


При использовании размещения new также необходимо убедиться, что используемый вами объект достаточно велик для создаваемого вами объекта. В этом случае так и должно быть, поскольку классы одинаковы, но static_assert сравнивающий размеры, гарантирует это и останавливает компиляцию, если это не так.


Один из способов исправить это - создать другой класс животных, который действует как держатель вашего класса животных (я переименовал его в Animal_Base в примере кода ниже). Это позволяет вам инкапсулировать изменение типа объекта, который представляет Animal. Изменение вашего кода на

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 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))) {}
};

а затем настраивая main для

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;
}

производит вывод

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

и это безопасно и портативно.

Ответ 2

1) Нет, это небезопасно по следующим причинам:

  • Поведение не определено и может отличаться для некоторых компиляторов.
  • Выделенная память должна быть достаточно большой, чтобы вместить вновь созданную структуру.
  • Некоторые компиляторы могут вызывать деструктор исходного объекта, даже если он виртуальный, что может привести к утечкам и сбоям.
  • В вашем коде деструктор исходного объекта не вызывается, поэтому он может привести к утечкам памяти.

2) Я наблюдал на MSVC2015, что dog.voice() будет вызывать Dog::voice без проверки действительной виртуальной таблицы. Во втором случае он проверяет виртуальную таблицу, которая была изменена на Cat::voice. Однако, как показали другие пользователи, некоторые другие компиляторы могут выполнять некоторые оптимизации и напрямую вызывать метод, который соответствует объявлению во всех случаях.

Ответ 3

У вас есть как минимум три проблемы с этим кодом:

  • Нет гарантии, что при вызове размещения new размер объекта, в котором вы строите новый объект, достаточен для того, чтобы удерживать новый объект.
  • Вы не вызываете деструктор объекта, используемого в качестве заполнителя
  • Вы используете объект Dog после его повторного использования.