удалите это и разместите новое из практически производного класса - PullRequest
2 голосов
/ 18 ноября 2011
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 с другим типом, фактически полученным из той же базы, когда ни один из них не имеет дополнительных членов над общей базой?В частности, разрешено ли компиляторам вызывать отслеживание статического типа или это технически не соответствует?

[EDIT] Некоторые люди указывали, что компиляторы могут вызывать неправильный деструктор, если derivedAсоздается в стеке.(1) Я не могу найти в стандарте ничего, что позволяло бы компиляторам это делать.(2) Это было помимо того, что я намеревался своим вопросом, поэтому я изменил код, чтобы показать, что derived не может быть помещено в стек.base все еще может быть в стеке.

Ответы [ 2 ]

3 голосов
/ 18 ноября 2011

Я думаю, что это явно не хорошо.

В качестве предисловия и время жизни объекта действительно можно закончить, вызвав деструктор, но вы только можете (и обязаны) создать новый объект вместо него того же типа :

{
  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), это не может быть в порядке.

(Возможно, если вы можете сделать дополнительные гарантии и для размеров объекта, такжечто касается гарантий выравнивания, мы можем подумать об этом дальше.)

Обновление: Даже если вы думаете, хорошо, значит, эти типы получены из одной и той же базы, поэтому вызовемДеструктор приносит виртуальное счастье, я все еще уверен, что это нарушение.В этом статическом параметре компилятор вполне может разрешить виртуальный вызов статически, поэтому вы нарушаете предположения компилятора, если вы изменяете динамический тип объекта, на который указывает &x.

Обновление 2: Еще одна мысль по тому же вопросу: статический тип из *&x известен, и я думаю, что вы должны уважать это.Другими словами, у компилятора нет оснований учитывать вероятность того, что статический тип локальной переменной изменится.

1 голос
/ 18 ноября 2011

Я почти уверен, что недействительный код по нескольким причинам:

  1. Если вставленный тип имеет разный размер, могут произойти плохие вещи (я не совсем уверен, но я думаю, что стандарт не дает много обещаний относительно размера типа, так что с теоретической точки зрения это может быть трудно доказать, что они имеют одинаковый размер (в практике они, скорее всего, будут))
  2. если тип переменной статически известен (возможно, из-за того, что он построен в стеке, но теоретически он может сделать то же самое, если увидит выделение и докажет, что указатель не мог быть изменен ) компилятор может свободно обрабатывать вызовы виртуальных методов (например, деструктор) и использовать их, что, очевидно, нарушит код
  3. Даже если тип переменной не является статически известным, я вполне уверен, что компилятор может предположить, что его тип не изменится в течение срока его службы (указатель не может измениться внутри функции, поэтому он должен иметь возможность предполагать, что Указанный тип также не имеет). Поэтому, хотя он не может статически разрешать методы, он может повторно использовать указатель vmt из предыдущих вызовов виртуальных методов (например, тот, который изменяет тип)

Редактировать: теперь, когда я думаю об этом, это не нарушает строгих правил наложения имен, так как после размещения new это указывает на несовместимый тип? Разумеется, в функции он снова не вызывается явным образом, но я не думаю, что можно гарантировать, что компилятор не получит доступ (хотя и маловероятно). В любом случае это будет означать, что компилятор может предположить, что такого рода действия не произойдут.

Редактировать: Когда я смотрел на новый стандарт C ++, я обнаружил, что [basic.life] (§3.8.5) дает то же самое, что и пример неопределенного поведения (это не так). на самом деле уничтожить объект, но я не понимаю, как это может улучшить ситуацию):

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

Это должно доказать, что это недопустимо.

...