Подтвердить что ты не робот

Если я изменю деструктор одного базового класса от не виртуального к виртуальному, что произойдет?

Я столкнулся с базовым классом, деструктор которого не является виртуальным, хотя базовый класс имеет 1 виртуальную функцию fv(). Этот базовый класс также имеет много подклассов. Многие из этих подклассов определяют свой собственный fv().

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

Я хочу изменить деструктор базового класса от не виртуального к виртуальному. Но я не уверен в последствиях. Итак, что будет? Что еще мне нужно сделать, чтобы убедиться, что программа отлично работает после того, как я ее изменил?

Последующие действия: После того, как я изменил деструктор базового класса из не виртуального в виртуальный, программа не смогла выполнить один тестовый пример.
Результат меня смущает. Поскольку, если деструктор базового класса не является виртуальным, тогда программа не будет использовать полиморфизм базового класса. Потому что, если нет, это приводит к поведению undefined. Например, Base *pb = new Sub.
Итак, я думаю, что если я изменю деструктор от не виртуального к виртуальному, он не должен вызывать никаких ошибок.

4b9b3361

Ответ 1

Виртуальность деструктора ничего не сломает в существующем коде, если нет других проблем. Он может даже решить некоторые проблемы (см. Ниже). Однако класс не может быть разработан как полиморфный, поэтому добавление виртуального к его деструктору позволяет использовать его как полиморфно-способное, что может быть нежелательно. Тем не менее вы должны иметь возможность безопасно добавлять виртуальность в деструктор, и это не должно вызывать проблем.

Объяснение

Полиморфизм допускает следующее:

class A 
{
public:
    ~A() {}
};

class B : public A
{
    ~B() {}

    int i;
};

int main()
{
    A *a = new B;
    delete a;
}

Вы можете взять указатель на объект типа A класса, который на самом деле имеет тип B. Это полезно, например, для разделения интерфейсов (например, A) и реализаций (например, B). Однако что произойдет на delete a;?

Часть объекта A типа A уничтожается. Но как насчет части типа B? Кроме того, у этой части есть ресурсы, и их нужно освободить. Ну, это утечка памяти прямо там. Вызывая delete a;, вы вызываете деструктор типа A (потому что A является указателем на тип A), в основном вы вызываете a->~a();. Деструктор типа B никогда не вызывается. Как это решить?

class A :
{
public:
    virtual ~A() {}
};

Добавив виртуальную диспетчеризацию к деструктору A (обратите внимание, что, объявив базовый деструктор виртуальным, он автоматически превращает все деструкторы всех производных классов в виртуальные, даже если они не объявлены как таковые). Затем вызов delete a; отправит вызов деструктора в виртуальную таблицу, чтобы найти правильный деструктор для использования (в данном случае типа B). Этот деструктор будет вызывать родительские деструкторы, как обычно. Аккуратно, правильно?

Возможные проблемы

Как вы видите, делая это, вы не можете сломать что-либо само по себе. Однако в вашем дизайне могут быть разные проблемы. Например, может быть ошибка, которая "полагалась" на не виртуальный вызов деструктора, который вы выставили, сделав его виртуальным, рассмотрите:

int main()
{
    B *b = new B;
    A *a = b;
    delete a;
    b->i = 10; //might work without virtual destructor, also undefined behvaiour
}

В основном обрезка объектов, но поскольку у вас не было виртуального деструктора до того, как B часть созданного объекта не была уничтожена, значит, выполнение функции i может работать. Если вы создали виртуальный деструктор, то он не существует, и он, скорее всего, сработает или сделает что-либо (undefined поведение).

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

Ответ 2

Посмотрите здесь,

struct Component {
    int* data;

    Component() { data = new int[100]; std::cout << "data allocated\n"; }
    ~Component() { delete[] data; std::cout << "data deleted\n"; }
};

struct Base {
    virtual void f() {}
};

struct Derived : Base {
    Component c;
    void f() override {}

};

int main()
{
    Base* b = new Derived;
    delete b;
}

Выход:

выделенные данные

но не удалено.

Заключение

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

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

Технические данные

Что происходит в примере, так это то, что, хотя Base имеет vtable, сам его деструктор не является виртуальным, а это значит, что всякий раз, когда вызывается Base::~Base(), он не проходит через vptr. Другими словами, он просто вызывает Base::Base() и что он.

В функции main() новый объект Derived выделяется и присваивается переменной типа Base*. Когда следующий оператор delete запускается, он фактически сначала пытается вызвать деструктор непосредственно переданного типа, который просто Base*, а затем освобождает память, занятую этим объектом. Теперь, поскольку компилятор видит, что Base::~Base() не является виртуальным, он не пытается пройти через vptr объекта d. Это означает, что Derived::~Derived() никогда никто не вызывает. Но так как Derived::~Derived() - это то место, где компилятор сгенерировал уничтожение Component Derived::c, этот компонент никогда не уничтожается. Поэтому мы никогда не видим напечатанных данных.

Если Base::~Base() были виртуальными, произойдет следующее: оператор delete d будет проходить через vptr объекта d, вызывая деструктор, Derived::~Derived(). Этот деструктор, по определению, сначала вызовет Base::~Base() (он автоматически генерируется компилятором), а затем уничтожит его внутреннее состояние, то есть Component c. Таким образом, весь процесс уничтожения завершился бы, как ожидалось.

Ответ 3

Это зависит от того, что делает ваш код, очевидно.

В общих чертах, создание деструктора базового класса virtual необходимо только в том случае, если у вас есть такое использование, как

 Base *base = new SomeDerived;
    // whatever
 delete base;

Наличие не виртуального деструктора в Base приводит к тому, что вышесказанное демонстрирует поведение undefined. Создание виртуального деструктора устраняет поведение undefined.

Однако, если вы сделаете что-то вроде

{   // start of some block scope

     Derived derived;

      //  whatever

}

тогда деструктор не является виртуальным, так как поведение корректно определено (деструкторы Derived и его базы вызывается в обратном порядке их конструкторов).

Если изменение деструктора с не от virtual до virtual приводит к сбою тестового примера, вам необходимо изучить тестовый пример, чтобы понять, почему. Одна из возможностей заключается в том, что тестовый пример основан на некотором конкретном заклинании поведения undefined, что означает, что тестовый случай ошибочен и может не преуспеть в разных обстоятельствах (например, при создании программы с другим компилятором). Однако, не увидев тестовый пример (или MCVE, который является его представителем), я бы с сомнением заявил, что он полагается на поведение undefined

Ответ 4

Это может сломать некоторые тесты, если кто-то из производного класса изменил политику собственности ресурсов класса:

struct A 
{ 
    int * data; // does not take ownership of data
    A(int* d) : data(d) {}
     ~A() { }
};

struct B : public A // takes ownership of data
{
    B(int * d) : A (d) {}
    ~B() { delete data; }
};

И использование:

int * t = new int(8);
{
    A* a = new B(t);
    delete a;
}
cout << *t << endl;

Здесь создание виртуального деструктора вызовет UB. Я не думаю, что такое использование можно назвать хорошей практикой.

Ответ 5

Вы можете "безопасно" добавить virtual в деструктор.

Вы можете исправить Undefined Behavior (UB), если вызывается эквивалент delete base, а затем вызывают правильные деструкторы. Если деструктор подкласса неисправен, вы меняете UB на другую ошибку.

Ответ 6

Одна вещь, которая может измениться, - это тип макета класса. Добавление виртуального деструктора может изменить класс от стандартного макета к классу нестандартного макета. Итак, все, что вы полагаетесь на класс, являющийся POD или стандартным типом макета, например

  • классы memcpy или memmve вокруг
  • перейти к функциям C

будет UB.

Ответ 7

Я знаю точно один случай, когда тип

  • используется как базовый класс
  • имеет виртуальные функции
  • деструктор не является виртуальным
  • создание виртуального разрушителя деструктора.

то есть когда объект соответствует внешнему ABI. Все COM-интерфейсы в Windows соответствуют всем четырем критериям. Это не поведение undefined, а неперспективные гарантии, связанные с конкретными конкретными действиями в отношении виртуального диспетчерского оборудования.

Независимо от вашей ОС, это сводится к "Правилу с одним определением". Вы не можете изменить тип, если вы не перекомпилируете каждый фрагмент кода, используя его, против нового определения.