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

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

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

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

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


person Yuan Wen    schedule 22.08.2016    source источник
comment
Вы пробовали это? : D   -  person qxz    schedule 22.08.2016
comment
Обратите внимание, что подклассы и виртуальные функции не обязательно подразумевают виртуальные деструкторы - виртуальный деструктор необходим только в том случае, если объекты должны быть разрушены полиморфно (через статический тип базового класса). Например, автоматическая переменная или экземпляр, управляемый shared_ptr, не нужны.   -  person Quentin    schedule 22.08.2016
comment
Да, пробовал. Но, к сожалению, в одном тестовом случае новая программа провалила. @ Qxz   -  person Yuan Wen    schedule 22.08.2016
comment
Что насчет поведения класса изменилось?   -  person qxz    schedule 22.08.2016
comment
Я бы сказал, что вы всегда должны объявлять деструкторы как виртуальные в тех классах, которые будут использоваться в качестве базовых классов. в противном случае вы, вероятно, получите UB (это UB - это как бомба замедленного действия, с ней можно пожить какое-то время, но однажды она взорвется)   -  person fgrdn    schedule 22.08.2016
comment
Программа слишком сложна для меня, чтобы разобраться. Не могли бы вы намекнуть на это? @qxz   -  person Yuan Wen    schedule 22.08.2016
comment
@fgrdn Я бы не согласился. Не все базовые классы предназначены для использования (и удаления) полиморфно, и отсутствие виртуального деструктора является для этого хорошим индикатором. Я бы посоветовал сделать деструктор защищенным в таких случаях.   -  person juanchopanza    schedule 22.08.2016
comment
Что ж, без виртуального деструктора удаление Base*, которое указывает на Derived, вероятно, вызовет UB. Таким образом, сделать его виртуальным будет означать, что он (должен) вести себя должным образом.   -  person qxz    schedule 22.08.2016
comment
Исходный базовый класс работает нормально. Но изменение деструктора с не виртуального на виртуальный вызывает ошибки. @ Qxz   -  person Yuan Wen    schedule 22.08.2016
comment
Обычное эмпирическое правило состоит в том, что всякий раз, когда у вас есть virtual метод, почти всегда вам нужен virtual деструктор (кроме того, если у вас есть единственный виртуальный метод, цена vptr уже оплачена, поэтому добавление дополнительного виртуального метода по сути бесплатно ).   -  person Matteo Italia    schedule 22.08.2016
comment
@YuanWen Можете ли вы предоставить пример кода (или ссылку на него) и дополнительную информацию о том, как именно программа ведет себя по-разному?   -  person qxz    schedule 22.08.2016
comment
@juanchopanza, вы никогда не гарантируете будущее этих базовых классов. Я имею в виду серьезные большие проекты с командной работой. я бы оставил класс без vtbl, только если этот класс не позволяет использовать себя в качестве базы в будущем.   -  person fgrdn    schedule 22.08.2016
comment
@YuanWen: это страшно. Правильно сформированная программа не должна изменять поведение, добавляя virtual в деструктор базового класса. В любом случае, если это полезно в вашем исследовании, обычно, когда у вас нет виртуального деструктора базового класса, происходит следующее: когда вы удаляете производный объект с помощью указателя на базу, производный деструктор не вызывается, и поля, добавленные в производный класс, не уничтожаются.   -  person Matteo Italia    schedule 22.08.2016
comment
@fgrdn Вы не можете ничего гарантировать, но вы можете указать, как будет использоваться класс, и вы можете защитить от удаления из базового класса, сделав деструктор защищенным. Обратите внимание, что я ничего не сказал об исключении vtbl. Я имел в виду только виртуальный деструктор.   -  person juanchopanza    schedule 22.08.2016
comment
@juanchopanza действительно - вы не можете гарантировать все. но представьте себе - у вас есть базовый класс, вы защищаете его деструктор. но затем кто-то в вашей команде создает другой класс (скажем, baseClassEnhancer) с общедоступным деструктором, но без виртуального ключевого слова (только потому, что он / она думает - это базовый класс, поэтому он должен иметь виртуальный деструктор). а затем сделать производным от этого другой класс. конечно, вы можете поговорить с этим товарищем по команде, чтобы не делать этого снова, но до того, как ваше приложение выдаст кучу исключений   -  person fgrdn    schedule 22.08.2016
comment
@fgrdn только потому, что он / она думает - это базовый класс, поэтому у него должен быть виртуальный деструктор, поэтому вам нужно сделать деструктор виртуальным, потому что есть кодеры, которые думают, что вам нужно сделать деструктор виртуальным? Похоже на программирование культа карго для поддержки программистов культа карго.   -  person juanchopanza    schedule 22.08.2016
comment
@fgdrn Если это произойдет, значит, вы не смогли правильно задокументировать свой код. Все хотят притвориться, что C ++ достаточно здравомыслящий, чтобы прояснить ваши намерения только с помощью кода из-за философии бла-бла, но на самом деле это не так. Если вы не хотите, чтобы класс наследовался, оставьте четкий комментарий в источнике вместо того, чтобы предполагать, что намерение будет четко передано наличием или отсутствием только виртуального деструктора. Если вы действительно хотите сделать это правильно, также объясните почему в своей документации.   -  person Jason C    schedule 22.08.2016
comment
@juanchopanza: создание виртуального деструктора в настоящее время не является проблемой с производительностью.   -  person fgrdn    schedule 22.08.2016
comment
@fgrdn Ваш счастливый подкласс получит ошибку времени компиляции при объявлении деструктора override, как и должно происходить всякий раз, когда вы предполагаете, что функция виртуальная.   -  person Quentin    schedule 22.08.2016
comment
@ jason-c вы можете найти множество проектов, библиотек, фреймворков, которые отлично работают, и у них нет документации или нет, но они не так хороши. только в идеальном мире все документируется.   -  person fgrdn    schedule 22.08.2016
comment
@Quentin, если подкласс ДЕЛАЕТ это сам. он может просто оставить деструктор как ~b1 () { /* do stuff */ }   -  person fgrdn    schedule 22.08.2016
comment
@fgdrn Вы найдете множество проектов, которые отлично работают ... и что, следовательно,? Это неуместно общее. Вы определили свою гипотетическую команду как команду, в которую входят люди, которые могут совершать эти ошибки. Так что для вашей команды в вашей среде это проблема документации. Для небольшой команды в более контролируемой среде определение надлежащей документации может измениться. Для ситуаций, помимо той, о которой мы говорим, с виртуальными деструкторами и намерением полиморфизма, это тоже может измениться. Я говорю о разговоре, который сейчас ведется в комментариях. :)   -  person Jason C    schedule 22.08.2016
comment
@fgrdn Это обычно тот момент, когда вам требуется, чтобы разработчики действительно знали, как правильно использовать свои инструменты.   -  person Quentin    schedule 22.08.2016
comment
Голосование за закрытие, поскольку симптомы (безопасный акт маркировки деструктора как virtual каким-то образом вводящие ошибки) указывают на UB, однако OP не предоставил MCVE или какой-либо код, поэтому спекулировать на этом бесполезно. Вопросы, требующие помощи по отладке (почему этот код не работает?), должны включать желаемое поведение, конкретную проблему или ошибку и кратчайший код. необходимо воспроизвести его в самом вопросе. Вопросы без четкой постановки проблемы не будут полезны другим читателям. См .: Как создать минимальный, полный и проверяемый пример.   -  person underscore_d    schedule 22.08.2016
comment
@underscore_d Не согласен. Вопрос ясный, интересный, по теме, хорошо написан и на него можно ответить, на него получены информативные, полезные и вполне достаточные ответы. Я допускаю, что это может быть не один из обычных бессмысленные отладочные вопросы, к которым вы, вероятно, уже привыкли, и это меня освежает. Фактически, этот вопрос полезен для других читателей, и не существует вселенной, в которой здесь применима выбранная вами причина (для начала, этот вопрос не требует помощи по отладке).   -  person Jason C    schedule 22.08.2016
comment
Иаааааааааааааааааааааааааа! Можете выложить код? Мне бы очень хотелось узнать, почему модульные тесты терпят неудачу.   -  person BЈовић    schedule 22.08.2016
comment
К сожалению, исходный код слишком сложен для публикации. @ BЈовић   -  person Yuan Wen    schedule 23.08.2016


Ответы (7)


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

Объяснение

Полиморфизм позволяет:

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 может сработать. Если вы сделали деструктор виртуальным, то он не существует и, скорее всего, выйдет из строя или сделает что-то еще (неопределенное поведение).

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

person Resurrection    schedule 22.08.2016
comment
delete a; в вашем последнем примере запускает UB, если деструктор A не виртуальный. Ничего не гарантируется относительно выживания i или чего-либо еще. - person Quentin; 22.08.2016
comment
Да потому что программа работает нормально. Поэтому я предполагаю, что он не будет реализовывать код, подобный последнему примеру Resurrection. @ Quentin - person Yuan Wen; 22.08.2016
comment
Что еще может вызвать такую ​​же проблему? @Quentin - person Yuan Wen; 22.08.2016
comment
В вопросе говорится, что базовый класс уже является полиморфным, поэтому какое бы предложение ни было, однако класс не может быть разработан как полиморфный, поэтому добавление virtual к его деструктору делает его способным к полиморфизму, что может быть нежелательно. должно быть, это неправильно. - person Ben Voigt; 22.08.2016
comment
@BenVoigt Я предполагаю, что он имел в виду, что он представит накладные расходы на хранение vtable, если она еще не существует, но да - это, к сожалению, в лучшем случае сформулировано и не актуально, если класс уже использовался полиморфно, что по сути подразумевает, что он имеет virtual функции (что, как упоминалось в другом месте, не означает, что требуется virtual деструктор, если пользователь с базовым указателем / ссылкой не может delete это). - person underscore_d; 26.08.2016

Взгляните здесь,

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 < / em>. Другими словами, он просто вызывает 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. Таким образом, весь процесс уничтожения завершился бы, как и ожидалось.

person Yam Marcovic    schedule 22.08.2016
comment
Что за непроверенная логика разрушения? Подробнее? @ Ям Маркович - person Yuan Wen; 22.08.2016
comment
@YuanWen: В текущем примере ~Derived и так ~Component. - person Jarod42; 22.08.2016
comment
Возможно, например, что один из ваших производных классов имеет член, что-то вроде Component в моем примере, чья логика уничтожения ошибочна в определенных контекстах, таких как ваш неудавшийся тест. Теперь, поскольку раньше он даже не вызывался, вы прошли тест, но, вероятно, у вас произошла утечка ресурсов и / или памяти. - person Yam Marcovic; 22.08.2016
comment
Вы имеете в виду, что в ~Component() это может быть delete data вместо delete [] data? Но если ~Component() не вызывается, результат может быть хуже, потому что вместо некоторого освобождения ничего не было освобождено. @YamMarcovic - person Yuan Wen; 22.08.2016
comment
Нет, ~Component() здесь отлично написано. Проблема в том, что без виртуального деструктора в Base он просто не вызывается, когда вы вызываете delete на Base*. - person Yam Marcovic; 22.08.2016
comment
Я знаю, но в моем случае базовый класс с виртуальным деструктором не удался, а не виртуальный деструктор завершился успешно. @ YamMarcovic - person Yuan Wen; 22.08.2016
comment
@YuanWen: что, вероятно, означает, что кто-то полагается на тот факт, что какой-то член данных просочился или некоторая логика уничтожения производного класса неисправна (и программа работает нормально, потому что она не вызывается по ошибке). - person Matteo Italia; 22.08.2016
comment
Еще раз, C ++ не определяет здесь поведение, так как вызывает базовый деструктор, но не производный. Это неопределенное поведение. - person Ben Voigt; 26.08.2016

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

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

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

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

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

{   // start of some block scope

     Derived derived;

      //  whatever

}

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

Если изменение деструктора с не virtual на virtual приводит к сбою тестового примера, вам необходимо изучить тестовый пример, чтобы понять, почему. Одна из возможностей заключается в том, что тестовый пример основан на каком-то конкретном заклинании неопределенного поведения, что означает, что тестовый пример ошибочен и может не быть успешным в различных обстоятельствах (например, при создании программы с другим компилятором). Однако, не видя тестового примера (или представляющего его MCVE), я бы не стал утверждать, что он ДЕЙСТВИТЕЛЬНО полагается на неопределенное поведение.

person Peter    schedule 22.08.2016

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

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;

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

person Month    schedule 22.08.2016
comment
Как же так? Когда вызывается деструктор A, указатель t остается действительным. - person Month; 23.08.2016
comment
Когда вызывается деструктор A, это соломинка. Согласно Стандарту, в вашем коде нет ничего, что вызывает деструктор A. Кажется, вы думаете, что полиморфное удаление, когда деструктор не виртуальный, откажется от производного класса и правильно уничтожит базовый подобъект, но то, что на самом деле говорит Стандарт, сильно отличается. - person Ben Voigt; 23.08.2016
comment
В первой альтернативе (удалить объект), если статический тип удаляемого объекта отличается от его динамического типа, статический тип должен быть базовым классом динамического типа удаляемого объекта и статическим type должен иметь виртуальный деструктор или поведение не определено - person Ben Voigt; 23.08.2016
comment
Спасибо за разъяснения, я действительно этого не знал. - person Month; 23.08.2016

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

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

person Jarod42    schedule 22.08.2016
comment
UB - это сокращение от неопределенного поведения? @ Jarod42 - person Yuan Wen; 22.08.2016
comment
Может быть, объясните, когда могло случиться UB? - person juanchopanza; 22.08.2016
comment
Исходный базовый класс работает нормально. Но изменение деструктора с не виртуального на виртуальный вызывает ошибки. Как это исправить, если я по-прежнему хочу, чтобы деструктор был виртуальным? @Juanchopanza - person Yuan Wen; 22.08.2016
comment
@YuanWen Если поведение изменится, это означает, что деструктор должен был быть виртуальным. Поэтому вам нужно исправить другие ошибки в вашем коде. - person juanchopanza; 22.08.2016
comment
Да, если создание виртуального деструктора вызывает ошибки, что-то кажется довольно подозрительным ... - person qxz; 22.08.2016
comment
Возможно, тот, кто написал это с самого начала, обнаружил, что если они украдкой сделают этот деструктор не виртуальным, они смогут притвориться, что исправили свои исходные ошибки. В таком случае: получайте удовольствие! - person Jason C; 22.08.2016

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

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

будет УБ.

person Jens    schedule 22.08.2016
comment
База имеет виртуальную функцию, так что это уже не POD или стандартный макет. - person Mat; 22.08.2016

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

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

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

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

person Ben Voigt    schedule 22.08.2016
comment
Если он удаляет свой производный класс, а затем, если этот класс сам освобождает другой объект O в своем деструкторе, и логика уничтожения O по какой-то причине не работает, тогда test сломается, сделав базовый деструктор виртуальным. Однако правильное решение - это исправить логику уничтожения, а не оставлять деструктор не виртуальным. - person Yam Marcovic; 27.08.2016
comment
@Yam: Вы когда-нибудь реализовывали IUnknown для COM-объекта? Или любой шаблон проектирования, в котором объекты освобождаются с помощью виртуальной функции-члена, которая не является деструктором? Когда спецификация интерфейса определяет, что тип имеет не виртуальный деструктор, вы не должны добавлять ключевое слово virtual, поскольку это нарушит правило одного определения. - person Ben Voigt; 28.08.2016