【问题标题】:Undefined behaviour on reinitializing object via placement new on this pointer通过在此指针上放置 new 重新初始化对象的未定义行为
【发布时间】:2018-07-20 07:42:41
【问题描述】:

我在 cppcon 上看到 Piotr Padlewski 的演示文稿说以下是未定义的行为:

int test(Base* a){
  int sum = 0;
  sum += a->foo();
  sum += a->foo();
  return sum;
}

int Base::foo(){
  new (this) Derived;
  return 1;
}

注意:假设 sizeof(Base) == sizeof(Derived)foo 是虚拟的。

显然这很糟糕,但我对为什么它是 UB 很感兴趣。我确实了解 UB 访问 realloced 指针,但他说,这是一样的。

相关问题:Is `new (this) MyClass();` undefined behaviour after directly calling the destructor? 上面写着“如果没有例外就可以” Is it valid to directly call a (virtual) destructor? 上面写着new (this) MyClass(); 的结果是UB。 (与上述问题相反)

C++ Is constructing object twice using placement new undefined behaviour? 它说:

程序可以通过重用存储来结束任何对象的生命周期 对象占用或显式调用析构函数 具有非平凡析构函数的类类型的对象。对于一个对象 具有非平凡析构函数的类类型,程序不是 需要在存储之前显式调用析构函数 占用的对象被重用或释放;但是,如果没有 显式调用析构函数,或者如果删除表达式 (5.3.5) 是 不用于释放存储,析构函数不得 隐式调用和任何依赖副作用的程序 析构函数产生的行为未定义。

听起来还不错。

我在Placement new and assignment of class with const member 中找到了新展示位置的另一个描述

如果,在对象的生命周期结束之后并且在存储之前 被占用的对象被重用或释放,一个新的对象是 在原始对象占用的存储位置创建,a 指向原始对象的指针,引用的引用 到原始对象,或者原始对象的名称将 自动引用新对象,并且一旦生命周期 新对象已启动,可用于操作新对象,如果:

  • 新对象的存储正好覆盖了原始对象占用的存储位置,并且

  • 新对象与原始对象的类型相同(忽略顶级 cv 限定符),并且

  • 原始对象的类型不是 const 限定的,并且,如果是类类型,则不包含任何类型为 const 限定或引用类型,以及

  • 原始对象是 T 类型的最衍生对象,而新对象是 T 类型的最衍生对象(也就是说,它们不是 基类子对象)。

这似乎解释了 UB。但真的是这样吗?

这不是说我不能拥有std::vector<Base> 吗?因为我假设由于其预分配std::vector 必须依赖placement-news 和明确的ctors。第 4 点要求它是最衍生的类型,Base 显然不是。

【问题讨论】:

  • std::vector<Base> Base 中绝对是最衍生的。不可能有实际上是Deriveds 的Bases。在您的new (this) Derived; 中,this 实际上是否有足够的内存来保存Derived
  • 如果Derived 有额外成员(sizeof 更大)会怎样?这应该让您了解当您执行此展示位置 new 时可能会发生什么
  • 还要考虑foo 是一个虚方法。对foo() 的第二次调用实际上会调用Derived::foo
  • 假设大小和虚拟 foo 相同。我现在补充了。我希望 Derived::foo 被调用,但它是 UB

标签: c++ language-lawyer destructor placement-new


【解决方案1】:

我相信伊丽莎白巴雷特布朗宁说得最好。让我数一数。

  1. 如果Base 不是可以轻易破坏的,我们就无法清理资源。
  2. 如果sizeof(Derived) 大于this 的动态类型的大小,我们将占用其他内存。
  3. 如果Base 不是Derived 的第一个子对象,那么新对象的存储空间将不会完全覆盖原始存储空间,并且最终还会破坏其他内存。
  4. 如果Derived 只是与初始动态类型不同的类型,即使大小相同,也比我们在cannot be used 上调用foo() 来引用新对象的对象。如果BaseDerived 的任何成员是const 合格的或者是引用,情况也是如此。您需要 std::launder 任何外部指针/引用。

但是,如果sizeof(Base) == sizeof(Derived)Derived 可以轻易破坏,则BaseDerived 的第一个子对象,而您实际上只有Derived 对象...这很好。

【讨论】:

  • std::launder 仍然需要。
  • 1 & 2:假设这不适用,因为它显然是错误的。 3:相同的尺寸 -> 为什么存储布局很重要? 4. 我真的不明白。旧对象被销毁。剩下的就是一些处于未定义状态的内存块。我向这个 blob 添加了一个新对象,通过 ctor 调用正确初始化它。为什么是这个UB?想法:a)CPU执行顺序(乱序)“缓存”常量值,但无论如何你都可以cast它们,对吧? b) 编译器优化缓存旧的foo 指针/数据并重用它。那么std::launder 究竟做了什么?什么是“与初始动态类型不同的类型”
  • @Flamefire a) 您可以抛弃 const,但不能使用生成的左值来访问对象值。 b) 就是这样,std::launder 告诉编译器不要使用这个缓存值。大多数派生对象类请参见eel.is/c++draft/intro.object#6
  • 所以struct A{const int a_; A(int a):a_(a){} }; A a(1); const_cast<int&>(a.a_) = 2;是UB?
  • @Flamefire 是的。
【解决方案2】:

关于你的问题

...因为我假设由于它的预分配 std::vector 必须依赖 安置新闻和明确的演员。第 4 点要求它是 Base 显然不是最衍生的类型。第 4 点要求它 是 Base 显然不是最衍生的类型。

,我认为误解来自术语“最衍生对象”或“最衍生类型”:

类类型对象的“最派生类型”是实例化该对象的类,无论该类是否有进一步的子类。考虑以下程序:

struct A {
    virtual void foo() { cout << "A" << endl; };
};

struct B : public A {
    virtual void foo() { cout << "B" << endl; };
};

struct C : public B {
    virtual void foo() { cout << "C" << endl; };
};

int main() {

    B b;  // b is-a B, but it also is-an A (referred  to as a base object of b).
          // The most derived class of b is, however, B, and not A and not C.
}

当您现在创建vector&lt;B&gt; 时,该向量的元素将是类B 的实例,因此元素的最派生类型将始终是B,而不是C(或Derived) 在你的情况下。

希望这能带来一些启示。

【讨论】:

    猜你喜欢
    • 2013-08-12
    • 2019-04-13
    • 1970-01-01
    • 1970-01-01
    • 2011-02-11
    • 1970-01-01
    • 1970-01-01
    • 2015-04-19
    • 1970-01-01
    相关资源
    最近更新 更多