Ответ 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
и это безопасно и портативно.