【问题标题】:Overriding an object in memory with placement new用放置 new 覆盖内存中的对象
【发布时间】:2019-09-06 17:40:30
【问题描述】:

我有一个对象,我想将其“转换”为另一个对象。为此,我在第一个对象上使用了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 方法? (您可以看到,我调用了相同的方法,但通过地址 ((&amp;dog)-&gt;voice()),一切正常。

【问题讨论】:

  • 我无法引用标准中的哪个地方说这是不允许的,但我可以说我在系统的两条底线中都得到了“WOOF I am a CAT”,这是一个很好的指标,表明这种行为是不可移植的。
  • 如果您需要这种行为,我将其描述为“对象似乎会改变它的类”,请考虑使用四态模式:en.wikipedia.org/wiki/State_pattern

标签: c++ placement-new


【解决方案1】:

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

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

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

dog.transform(&amp;dog); 之后使用dog.voice(); 是未定义的行为。由于您已重用其存储而不破坏它,因此您有未定义的行为。假设您确实在 transform 中销毁了 dog,以摆脱您仍未摆脱困境的那一点未定义行为。在 dog 被销毁后使用它是未定义的行为。您需要做的是捕获指针放置新返回并从那时起使用该指针。您也可以在 dog 上使用 std::launderreinterpret_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

而且这是安全和便携的。

【讨论】:

  • 说得很好,+1 用于指出更多深奥的选项,例如std::launder。我唯一要补充的是建议使用标准多态性,而不是……随便你叫什么transform()
  • @Kevin 我正在考虑它并决定做得更好,并添加了一个示例,说明 OP 如何获得他们正在寻找的行为。
【解决方案2】:

1) 不,这是不安全的,原因如下:

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

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

【讨论】:

  • 当您说行为不可移植时,您需要解释原因。析构函数已经是虚拟的了。
  • 我的意思是一些编译器的行为不一样。 “未定义的行为”可能会更好。我会编辑答案。
  • 请注意有问题的代码中有一个虚拟析构函数
  • @NathanOliver 是的。我提到了需要尊重的一般规则。我会说得更清楚。
【解决方案3】:

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

  • 无法保证在调用放置 new 时,您正在构建新对象的对象的大小足以容纳新对象
  • 您没有调用用作占位符的对象的析构函数
  • Dog 对象的存储已被重用后使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-08
    • 1970-01-01
    相关资源
    最近更新 更多