使用放置 new 覆盖内存中的对象

Overriding an object in memory with placement new

本文关键字:内存 对象 覆盖 new      更新时间:2023-10-16

我有一个对象,我想把它'转换'成另一个对象。为此,我在第一个对象上使用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

我的问题是:

  1. 此操作是否被视为安全,或者它是否使对象处于不稳定状态?
  2. 转换后我称之为dog.voice().它正确地打印了CAT的名字(它现在是一只猫),但仍然写WOOF I am a,即使我认为它应该调用Catvoice方法?(你可以看到我调用了相同的方法,但通过地址((&dog)->voice()),一切都正常工作。

此操作是否被认为是安全的,还是使对象处于不稳定状态?

此操作不安全,并会导致未定义的行为。CatDog具有非平凡的析构函数,因此在重用存储cat之前,dog必须调用它们的析构函数,以便正确清理以前的对象。

转换后,我称之为dog.voice().我正确打印了CAT的名字(它现在是一只猫),但仍然写WOOF I am a,甚至很难,我会认为它应该叫Catvoice方法?(您可以看到的是我调用相同的方法,但通过地址((&dog)->voice()),一切都正常工作。

dog.transform(&dog);后使用dog.voice();是未定义的行为。 由于您重用了其存储而不破坏它,因此您具有未定义的行为。 假设你确实在transform中摧毁了dog,以摆脱你仍然没有摆脱困境的那一点未定义的行为。 在销毁后使用dog是未定义的行为。 您所要做的就是捕获指针放置新返回值,并从此使用该指针。 您还可以在dog上使用std::launder,并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'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

这是安全和便携的。

此代码至少存在三个问题:

  • 不能保证当放置 new 时称为 new 时,您在其中构建新对象的对象的大小足以容纳新对象
  • 您没有调用用作占位符的对象的析构函数
  • 在重复使用Dog对象的存储后,可以使用该对象。

1) 不,由于以下原因,这是不安全的:

  • 该行为未定义,对于某些编译器可能会有所不同。
  • 分配的内存需要足够大,以容纳新创建的结构。
  • 某些编译器可能会调用原始对象的析构函数,即使它是虚拟的,这会导致泄漏和崩溃。
  • 在代码中,不会调用原始对象的析构函数,因此可能会导致内存泄漏。

2)我在MSVC2015上观察到dog.voice()会在不检查实际虚拟表的情况下调用Dog::voice。在第二种情况下,它检查虚拟表,该表已被修改为Cat::voice。但是,正如其他用户所经历的那样,其他一些编译器可能会执行一些优化,并在所有情况下直接调用与声明匹配的方法。