【问题标题】:delete this and placement new of virtually derived class删除这个并放置新的虚拟派生类
【发布时间】:2011-11-17 23:51:58
【问题描述】:
class base {
    int a;
protected:
    template<class T>
    class derived;
public:
    base() {}
    virtual ~base() {}
    virtual void func() {}
    static base* maker();
};

template <class T>    
class base::derived 
    : public base
{
public: 
    derived() {}
    virtual ~derived() {}
    virtual void func() {
        this->~derived(); //<--is this legal?
        new (this) derived<int>(); //<--is this legal?
    }
};

base* base::maker() {
    return new derived<double>();
}

int main() {
    base* p = base::maker(); //p is derivedA<double>
    p->func(); //p is now derivedA<int>
    delete p; //is the compiler allowed to call ~derived<double>()?
}

这是我的代码的简短、自包含、正确(可编译)示例(基本上是为了我自己的成长而重新发明any_iterator)。

问题归结为:当共享基数上没有任何其他成员时,销毁this 并使用从同一基数虚拟派生的不同类型重构this 是否是未定义的行为?具体来说,编译器是否允许调用静态类型的跟踪,或者这在技术上不符合标准?

[编辑] 一些人指出,如果在堆栈上创建 derivedA,编译器可能会调用不正确的析构函数。 (1) 我在标准中找不到任何允许编译器这样做的东西。 (2) 除了我的问题打算之外,我已经更改了代码以显示derived 不能放在堆栈上。 base 仍然可以在堆栈中。

【问题讨论】:

  • 正确与否无关紧要。头脑正常的人不会这样做。即使它确实有效,也将是维护的噩梦,因此它永远不会在实际代码中发生。
  • 离题:为什么要转换成巨大的指针? Placement-new 需要一个 void 指针。也许您的案例是动态转换为无效的候选者! void * p = dynamic_cast&lt;void*&gt;(this); new (p) T;
  • 我很确定如果用于堆栈上声明的变量,这会严重破坏。
  • @KerrekSB 我是否感觉到您正试图从本周早些时候从一个问题中学到的东西中获得乐趣? :)
  • @MatteoItalia:不,这完全没问题(见我的回答)。 w00te:有那么一分钟,我以为我们可能会在这里做点什么,但现在我确信它没有任何意义:-(

标签: c++ templates virtual placement-new


【解决方案1】:

我认为这显然不行。

作为序言,对象的生命周期确实可以通过调用析构函数来结束,但你只能在其位置构造一个相同类型的新对象

{
  Foo x;
  x.~Foo();
  ::new (&x) Foo;
}  // x.~Foo() must be a valid call here!

请记住,在作用域结束时将调用析构函数!

但在您的情况下,您正在构建一个 完全不同的对象:

::new (&x) Bar;   // Bar = DerivedA<int>

很明显,如果sizeof(Bar) 超过sizeof(Foo) 这是不行的。

(也许如果您可以对对象大小以及对齐保证做出额外的保证,我们可以进一步考虑。)

更新:即使你在想,好吧,所以这些类型是从同一个基派生的,所以调用析构函数会带来虚拟的快乐,我仍然很确定这是一种违规行为。在这种静态设置中,编译器可以很好地静态解析虚拟调用,因此如果您更改 &amp;x 指向的事物的动态类型,您将打破编译器的假设。

更新 2: 关于同一问题的另一个想法:*&amp;x静态类型 是已知的,我认为您必须尊重这一点。换句话说,编译器没有理由考虑局部变量的静态类型发生变化的可能性。

【讨论】:

  • 在我的 OP 中指出,它们保证大小相同。关于您的更新:在符合标准的编译器中是否允许这样做?我违反规范还是编译器? (假设它不起作用。我还没有实际测试过。)
  • @MooingDuck:如果您的示例是字面意思,那么您不需要任何虚拟层次结构或任何析构函数,所以您可能没问题。我猜我的回答针对的是更一般的情况。
  • 我重读了 Matteo Italia 并意识到他在我的 SSCCE 中发现了在我的真实代码中不是的潜在错误,因此我更新了问题以澄清 derived 不能在堆栈上创建。不过,我认为这不会影响您的回答。
  • 在真实的代码中,base 包含一个缓冲区,derived 正在将T 放置到新的缓冲区中,虚拟成员作用于该T。需要虚拟层次结构。
  • @Mooing:如果你有虚函数,为什么在基类中有一个缓冲区,而不是在派生类中有一个正确类型的值?
【解决方案2】:

我很确定这不是有效的代码,原因如下:

  1. 如果插入的类型大小不同,就会发生不好的事情(我不太确定,但我认为标准并没有对类型的大小做出太多承诺,所以从理论上讲它可能很难证明它们具有相同的尺寸(通过在实践中它们很可能会))
  2. 如果变量的类型是静态已知的(可能是由于它是在堆栈上构造的,但理论上它可以做同样的事情,如果它可以看到分配并证明指针不能被修改) 编译器可以随意静态解析虚拟方法调用(例如析构函数)并使用它们,这显然会破坏代码
  3. 即使变量的类型不是静态已知的,我也很确定编译器可以假设它的类型在它的生命周期内不会改变(指针在函数内部不能改变,所以它应该能够假设指向的类型也不)。因此,虽然它不能静态解析方法,但它可能会重用之前调用虚拟方法(例如更改类型的方法)中的 vmt 指针

编辑:现在我想这不会违反严格的别名规则,因为在放置 new 之后 this 指向不兼容的类型?授予它不会再次在函数中显式访问,但我认为不能保证编译器不会插入访问(尽管可能性很小)。无论如何,这意味着编译器可以假设这种行为不会发生。

编辑:在查看新的 C++ 标准时,我发现 [basic.life] (§3.8.5) 给出了与未定义行为示例基本相同的内容(它没有t 实际上破坏了对象,但我不明白这如何能让事情变得更好):

#include<cstdlib>
structB{
    virtual void f();
    void mutate();
    virtual ~B();
};
struct D1:B { void f(); };
struct D2:B { void f(); };
void B::mutate(){
    new(this)D2; //reuses storage—ends the lifetime of *this
    f(); //undefined behavior
    ...=this; //OK, this points to valid memory
}
void g(){
    void* p = std::malloc(sizeof(D1) + sizeof(D2));
    B* pb = new(p)D1;
    pb->mutate();
    &pb; //OK: pb points to valid memory
    void* q = pb; //OK: pb points to valid memory
    pb->f(); //undefined behavior, lifetime of *pb hasended
}

这应该证明这是不允许的。

【讨论】:

  • (1) 就问题而言,对象 的大小相同。 (2/3) 问题的核心是是否允许符合标准的编译器这样做。
  • @MooingDuck:从标准(草案)中提取的示例指向允许编译器这样做的方向。
猜你喜欢
  • 2012-12-20
  • 2014-08-27
  • 2015-10-23
  • 2019-11-04
  • 1970-01-01
  • 1970-01-01
  • 2011-01-15
  • 2013-10-07
相关资源
最近更新 更多