用新的位置覆盖内存中的对象

Guy*_*afe 15 c++ placement-new

我有一个要“转换”为另一个对象的对象。为此,我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();
  }
};
Run Code Online (Sandbox Code Playgroud)

您会看到,当与a一起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;
}
Run Code Online (Sandbox Code Playgroud)

结果是:

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
Run Code Online (Sandbox Code Playgroud)

我的问题是:

  1. 该操作被认为是安全的,还是会使物体处于不稳定状态?
  2. 转换后,我致电dog.voice()。它可以正确打印名称CAT(现在是一只猫),但是仍然可以写入WOOF I am a,即使我以为它应该调用Catvoice方法?(您可以看到,我调用了相同的方法,但是通过地址((&dog)->voice()),一切正常。

Nat*_*ica 15

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

此操作不安全,并且会导致不确定的行为。 Cat并且Dog具有非平凡的析构函数,因此在可以重复使用存储之前catdog必须调用它们的析构函数,以便正确清理先前的对象。

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

使用dog.voice();after dog.transform(&dog);是未定义的行为。由于您在不破坏存储的情况下重复使用了其存储,因此您具有未定义的行为。可以说,您确实进行了破坏dogtransform以摆脱那些尚未定义的行为,但您仍未摆脱困境。dog销毁后使用是未定义的行为。您需要做的就是捕获指针放置新的返回值,然后使用该指针。你也可以使用std::launderdogreinterpret_cast你转换它的类型,但因为你失去所有的封装是不值得。


您还需要确保在使用新放置时,要使用的对象对于要构造的对象足够大。在这种情况下,应该是这样,因为类是相同的,但是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))) {}
};
Run Code Online (Sandbox Code Playgroud)

然后调整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;
}
Run Code Online (Sandbox Code Playgroud)

产生输出

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
Run Code Online (Sandbox Code Playgroud)

这是安全且便携的。


Gil*_*llé 6

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

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

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


Ser*_*eyA 6

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

  • 不能保证在调用placement new时要构造的对象的大小足以容纳新对象
  • 您没有在调用用作占位符的对象的析构函数
  • 您可以在Dog对象的存储被重用之后使用它。