Хорошо ли определен переопределяющий деструктор для автоматического объекта?

#c #reference #polymorphism #language-lawyer #destructor

Вопрос:

У меня была ситуация, когда я хотел импортировать вызов после другого вызова из вызывающей функции. Я решил переопределить виртуальный деструктор для этой цели:

 #include <iostream>

struct type {
    virtual ~type() {
        std::cout << "ordinary" << std::endl;
    }
    void method() {
        struct method_called : type {
            virtual ~method_called() override {
                std::cout << "method called" << std::endl;
            }
        };

        this->~type();

        new (this) method_called{};
    }
};

int main() {
    
    std::cout << "ordinary expected" << std::endl;

    {
        type obj;
    }

    std::cout << "method expected" << std::endl;

    {
        type obj;

        obj.method();
    }

    std::cout << "method expected" << std::endl;

    type* pobj = new type{};

    pobj->method();

    delete pobj;
}
 

Похоже, переопределенный деструктор вызывается только с использованием динамического распределения. Это специально?

GCC godbolt.

Комментарии:

1. После вызова this->~type() использование this имеет неопределенное поведение. Ваш код каким-то образом не преобразует экземпляр a type в экземпляр (локально определенного) method_type , который (кажется) является тем, что вы пытаетесь сделать.

2. obj.method(); не изменяет obj тип. это все еще type так . Компилятор вызывает деструктор type::~type после } напрямую, не используя vtbl, так как он знает тип obj .

3. @Питер, я все еще не убежден — не могли бы вы, пожалуйста, написать ответ? Возможно, цитируя стандарт. Или, может быть, свяжите другой аналогичный вопрос, если он есть.

4. @AnArrayOfFunctions Хотя слишком многие языковые юристы подразумевают обратное, стандарт не описывает каждый случай неопределенного поведения. Существует множество случаев, когда поведение не определено пропуском, т. Е. Когда стандарт не определяет никаких ограничений на то, что происходит. Это связано с тем, что члены комитета по стандартам-простые смертные, которые не ожидают такого использования (если никто не ожидает кода, который пытается выполнить X, невозможно указать ограничения на то, что происходит из-за таких попыток, и в равной степени невозможно указать, что ограничений нет, т. Е. сделать его неопределенным).

Ответ №1:

 this->~type();
 

Любое разыменование любого указателя, указывающего на объект или переменную, ссылающуюся на объект, существующий до этой строки, является неопределенным поведением, как только вы это сделаете.

Это включает в себя автоматический деструктор хранилища.

Чтобы избежать неопределенного поведения main после этого, вам придется вызывать exit или аналогичным образом никогда не возвращаться из области, в которой существует переменная.

К тому времени, когда деструктор будет завершен, срок службы объектов закончится. Почти любое использование объекта после этого запускает UB. Вы можете повторно использовать хранилище:

     new (this) method_called{};
 

Это создает отдельный объект в хранилище, на который this ссылается. Если это хранилище имеет достаточное выравнивание и размер, это нормально. Это чрезвычайно сложно сделать правильно, потому что один из немногих способов безопасно получить указатель на этот вновь созданный объект-записать возвращаемое значение в новое, как это:

 auto* pmc = new (this) method_called{};
 

ты этого не делаешь.

 {
    type obj;

    obj.method();
}
 

это неопределенное поведение. Стандарт не ограничивает действия программы, созданной вашим компилятором, если этот код выполняется или будет выполняться в любой момент выполнения ваших программ.

 type* pobj = new type{};

pobj->method();

delete pobj;
 

как и это.