我有一个对象,我想将其“转换”为另一个对象。为此,我在第一个对象上使用了 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();
}
};
您可以看到,当使用 transform
调用 Dog
时,它会在给定地址之上创建一个新的 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
我的问题是:
- 此操作是否被认为是安全的,还是会使对象处于不稳定状态?
- 转换后我调用
dog.voice()
。它正确地打印出名称CAT
(它现在是一只猫),但仍然写出WOOF I am a
,即使我认为它应该调用猫
的voice
方法? (你可以看到我调用了同样的方法,但是通过地址((&dog)->voice()
),一切正常。
最佳答案
Does this operation considered safe, or does it leave the object in unstable state?
此操作不安全,会导致未定义的行为。 Cat
和 Dog
有非平凡的析构函数,所以在你可以重用存储 cat
和 dog
之前你必须调用它们的析构函数,因此前一个对象被正确清理。
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.
在 dog.transform(&dog);
之后使用 dog.voice();
是未定义的行为。由于您已重用其存储而不破坏它,因此您有未定义的行为。假设您确实在 transform
中销毁了 dog
以摆脱那些您仍未摆脱困境的未定义行为。在 dog
被销毁后使用它是未定义的行为。您需要做的是捕获指针放置新返回并从那时起使用该指针。您还可以在 dog
上使用 std::launder
和 reinterpret_cast
到您将其转换为的类型,但它不值得,因为您丢失了所有封装.
您还需要确保在使用placement 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'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))) {}
};
然后将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
而且这是安全和便携的。
关于c++ - 用放置 new 覆盖内存中的对象,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55711220/