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

时间:2019-04-16 15:00:53

标签: 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();
  }
};

您会看到,用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 I am a,即使我以为它应该调用Cat的{​​{1}}方法? (您可以看到,我调用了相同的方法,但是通过地址(voice),一切正常。

3 个答案:

答案 0 :(得分:14)

  

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

此操作不安全,并且会导致未定义的行为。 CatDog具有非琐碎的析构函数,因此在重新使用存储catdog之前,必须调用它们的析构函数,以便正确清除先前的对象。 / p>

  

转换后,我叫dog.voice()。我正确打印了CAT的名字(现在是猫),但仍然写着WOOF I am a,即使我很难理解,它也应该称呼Cat的{​​{1}}方法? (您可以看到,我调用了相同的方法,但是通过地址(voice),一切正常。

(&dog)->voice()之后使用dog.voice();是未定义的行为。由于您在不破坏存储的情况下重复使用了其存储,因此您具有未定义的行为。假设您确实销毁了dog.transform(&dog);中的dog,以摆脱那些尚未定义的行为。 transform被销毁后使用是未定义的行为。您需要做的就是捕获指针放置新的返回值,然后使用该指针。您还可以将dog上的std::launderdog一起使用,以将其转换为要转换的类型,但这不值得,因为您丢失了所有封装。


还需要确保在使用new放置时,要使用的对象对于要构造的对象足够大。在这种情况下,应该是因为类是相同的,但是reinterpret_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))) {} }; 调整为

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

这是安全且便携的。

答案 1 :(得分:6)

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

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

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

答案 2 :(得分:6)

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

  • 不能保证在调用placement new时,您要在其中构造新对象的大小足以容纳新对象
  • 您没有调用用作占位符的对象的析构函数
  • <?php namespace Gloudemans\Shoppingcart\Facades; use Illuminate\Support\Facades\Facade; class Cart extends Facade { /** * Get the registered name of the component. * * @return string */ protected static function getFacadeAccessor() { return 'cart'; } } ?> 对象的存储已被重用之后,您可以使用它。