LINUX.ORG.RU

Виртуальные деструкторы

 


0

3

Всем привет!

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

Такой код у меня корректно отработал

#include <iostream>

using namespace std;

class A
{
	public:
		A(int a) : a(a){cout<<a<<endl;}
		virtual ~A() =0;
		
	private:
		int a;
};
A::~A()
{
	cout<<"~A"<<endl;
}

class B : public A
{
	public:
		B(int b) : A(b-10), b(b){cout<<b<<endl;}
		virtual ~B(){cout<<"~B"<<endl;}
		
	private:
		int b;
};

class C : public B
{
	public:
		C(int c) : B(c-10),c(c){cout<<c<<endl;}
		virtual ~C(){cout<<"~C"<<endl;}
		
	private:
		int c;
};

int main()
{
	A* f=new C(30);
	delete f;
	C g(67);
	return 0;
}

★★

Виртуальный деструктор привносит таблицу виртуальных функций.

Т.е. по сути - удорожает вызов деструктора на одно разыменование указателя (частично лечится оптимизатором, если в точке удаления понятен тип обьекта).

Ну и даёт оверхэд по памяти на sizeof(void*) на большинстве платформ (де юре implementation defined, дефакто никто ничего умнее не придумал).

P.S. композиция лучше наследования.

pon4ik ★★★★★
()

Каковы будут последствия того, что все деструкторы всех классов всегда делать виртуальными

А они и так будут виртуальными, если деструктор базового класса - виртуальный. Тут разницы с методами нет. А последствия простые - они все поочередно будут вызваны.

anonymous
()

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

fluorite ★★★★★
()
Ответ на: комментарий от pon4ik

Насколько это критично? вроде как от этого класс не становится сильно толстым и сильно медленным. Если только постоянно не идет создание и удаление объектов, но я пока с ходу не могу прикинуть ситуацию, где такое поведение будет нужно.

aido ★★
() автор топика
Ответ на: комментарий от aido

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

m0rph ★★★★★
()
Ответ на: комментарий от no-such-file

Сладкое лучше солёного.

Не совсем. Использование наследования влечет за собой дополнительные трудности. Но само по себе наследование, как таковое, реально требуется для весьма узкого круга моделируемых сущностей. Обычно бывает достаточно, чтобы класс имел функции с определенными именами - т.е. по сути, реализовывал некий интерфейс. При чем совсем не обязательно от него наследоваться, как это сделано в Java. Интерфейсы в Go или трейты в Rust хорошие примеры того, как можно обойтись без наследования. А так как в C++ всего этого нет и приходится придерживаться таких странных правил, вроде «композиция лучше наследования».

m0rph ★★★★★
()
Последнее исправление: m0rph (всего исправлений: 2)

Вопрос намного шире

ИМХО,

Любую функцию-челн класса, в т. ч. конструктор и деструктор, нужно делать виртуальными за исключением случаев, когда ты точно можешь объяснить, почему это тебе мешает.

Из «мешающего» уже было упомянуто требования к памяти объекта, скорость выполнения (это вообще можно хоть как-то заметить), еще бывают нужны статические функции. Кстати, AFAIK в Java все функции-члены классов и так виртулаьне.

Кто не согласен - дискасс.

Kroz ★★★★★
()
Последнее исправление: Kroz (всего исправлений: 1)
Ответ на: комментарий от anonymous

Причем этот оверхед только на класс, а не на каждый объект.

Как ты себе это представляешь?
Какая функция вызовется в примере ниже: первая или вторая?

class Parent{
public:
  virtual void fn() { /* Функция 1 */ };
};

class Child: public Parent{
public:
  virtual void fn() { /* Функция 2 */ };
};

Parent *a;
Child  *b = new Child();

a = b;
a->fn();

Kroz ★★★★★
()
Последнее исправление: Kroz (всего исправлений: 1)
Ответ на: Вопрос намного шире от Kroz

Любую функцию-челн класса, в т. ч. конструктор и деструктор, нужно делать виртуальными за исключением случаев, когда ты точно можешь объяснить, почему это тебе мешает.

Мощно. Внушаить. Особенно про виртуальные конструкторы.

Обосновать сможете?

eao197 ★★★★★
()
Ответ на: комментарий от eao197

Обосновать сможете?

Уберегает от man-made mistake: когда забыли виртуализировать нужную функцию, кейс с виртуальными деструкторами туда же и т. п.; такие ошибки ловятся очень сложно. Плюсы может и небольшие, но, поскольку минусы стремяться к нулю, имеет смысл взять за практику.

А теперь, с позволения, спрошу, есть ли контр-аргументы?

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

Уберегает от man-made mistake

Вот собственно из-за этого и возник вопрос.

aido ★★
() автор топика
Ответ на: комментарий от anonymous

Ага, и как же в рантайме понять, какой же всё таки метод дёргать?

pon4ik ★★★★★
()
Ответ на: Вопрос намного шире от Kroz

в Java все функции-члены классов и так виртулаьне

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

требования к памяти объекта, скорость выполнения

да кому это в C++ нужно?..

anonymous
()
Ответ на: комментарий от aido

Если 90% это i/o или сплошной гуй (==90% i/o) то никогда вообще не заметишь разницы никакой.

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

pon4ik ★★★★★
()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от aido

Кстати спорный вопрос. У меня был случай, когда подобные циферки роляли. И под х32 решение внезапно начинало падать ибо вылезало за доступную виртуальную память.

pon4ik ★★★★★
()
Ответ на: комментарий от anonymous

мощна! в коммитет подавал предложение? далеко отправили?

+ eao197
Так, ок, про конструктор свою ошибку понял. Мало спал сегодня.
Как на счёт остального?

Kroz ★★★★★
()
Последнее исправление: Kroz (всего исправлений: 1)
Ответ на: комментарий от anonymous

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

Можно пару кейсов, пожалуйста?

И, да, я не говорил, что не-виртуальные функции совсем не нужны. Я сказал что это редкость. И потому по дефолту виртуализировать "... за исключением случаев, когда ты точно можешь объяснить, почему это тебе мешает."

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

А теперь, с позволения, спрошу, есть ли контр-аргументы?

Как минимум, два.

Во-первых, скорость работы. Невиртуальный вызов заинлайнить намного проще. И даже когда он не инлайнится, все равно простой call будет быстрее, чем call через таблицу виртуальных функций.

Во-вторых, виртуальные методы — это один из способ реализации полиморфизма. Соответственно, применяться они должны для ситуаций, где требуется уточнение/изменение поведения в наследнике. В противном же случае люди сперва начинают страдать ООП головного мозга, а затем пишут что-то вроде «Меня любил ООП, но не выполнил обещаний, и теперь меня любит ФП».

eao197 ★★★★★
()
Ответ на: комментарий от m0rph

Интерфейсы в Go или трейты в Rust хорошие примеры того, как можно обойтись без наследования.

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

DarkEld3r ★★★★★
()
Ответ на: Вопрос намного шире от Kroz

Кто не согласен - дискасс.
AFAIK в Java все функции-члены классов и так виртулаьне.

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

DarkEld3r ★★★★★
()
Ответ на: комментарий от aido

На винде не вышло как-то :)

Ну и для некоторых задач даже начало свопа это уже провал.

Вообщем если ты не планируешь работать с миллионами мелких обьектов размером порядка указателя, про память эту можно не париться.

Но это всё касабельно синтаксиса в вакууме и влияния сего на конечный продукт по нефункциональным требованиям.

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

pon4ik ★★★★★
()
Последнее исправление: pon4ik (всего исправлений: 1)
Ответ на: комментарий от eao197

Во-первых, скорость работы.

Перформанс тест делали? В 99% случаев падение скорости составляет 10^-18. То есть вообще не аргумент.

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

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

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

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

Элементарно ловятся при соблюдении простейшего правила: использовать override. Плюс включение предупреждений компилятора.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Если оно устраивает, то проще и будет взять джаву или шарп

Не проще, ибо фичи C++ не ограничиваются простотой отказа от виртуализации функций.

Еще аргументы?

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

Перформанс тест делали? В 99% случаев падение скорости составляет 10^-18. То есть вообще не аргумент.

А вы? Делали ли вы тест, где все, что вы используете, реализуется на виртуальных методах?

Вопрос в другом: почему бы им не применяться и в других случаях?

Возможность использовать фичу «виртуальный метод» разве автоматически означает необходимость использовать фичу «виртуальный метод»? Мне казалось, что использование чего-то нужно обосновывать. Тем более, что за этим использованием кроются накладные расходы.

eao197 ★★★★★
()
Ответ на: комментарий от Kroz

Не проще, ибо фичи C++ не ограничиваются простотой отказа от виртуализации функций.

Проще, ибо фичи джавы не ограничиваются простотой виртуализации функций.

Еще аргументы?

Выше были.

Ещё для размышлений добавлю такой момент: выбор С++ зачастую означает именно то, что мы хотим выжимать скорость (ну или это легаси). Плюс на целевых платформах бывает всё не так здорово с ресурсами или оптимизациями. Например, если одной из «платформ» является emscripten, то становится важна и скорость и память. Хотя казалось бы, это не какой-нибудь «микроконтроллер».

А ещё это хорошо для самодокументации кода. Если я вижу virtual/override, то предполагаю, что оно по назначению использоваться будет.

DarkEld3r ★★★★★
()
Последнее исправление: DarkEld3r (всего исправлений: 1)
Ответ на: комментарий от DarkEld3r

Элементарно ловятся при соблюдении простейшего правила: использовать override. Плюс включение предупреждений компилятора.

И как меня это спасет от кейса вверху, если я забуду сделать функцию виртуальной, и буду биться головой об стенку, так как будет вызываться первая функция, когда я ожидаю вызов второй?

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

И как меня это спасет от кейса вверху, если я забуду сделать функцию виртуальной

Нормально спасёт, если код нормального качества. Тогда будет интерфейс, где забытая виртуальность выглядит странно. Иначе можно пойти дальше и спрашивать «что меня спасёт от копипаста функции из одного класса в другой, если я забуду поправить реакизацию?».

Ну и тесты, да.

DarkEld3r ★★★★★
()
Ответ на: комментарий от Kroz

Это вообще-то азы C++. Объекты НЕ ХРАНЯТ таблицу виртуальных функций. Хранят только указатель на нее. Так что НИКАКОГО оверхеда по памяти, занимаемой объектом не будет (если в объекте уже была хоть одна вирт. функция). Дальше не буду объяснять, это азы.

anonymous
()
Ответ на: комментарий от DarkEld3r

Тогда будет интерфейс, где забытая виртуальность выглядит странно.

Когда у тебя винегрет из виртуальных и не-виртуальных функций, то странной не покажется ни одна.

Тесты - да. Но покрытие тоже зачастую с пробелами.

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

Kroz ★★★★★
()
Ответ на: комментарий от anonymous

А мне это зачем? Это Kroz-у нужно показать.

Ты на мой пост отвечал? :)

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

Можно пару кейсов, пожалуйста?

Чего, работы jvm? Где тип объекта известен - там вставляется прямой вызов (это и компилятор C++ делает), где неизвестен, а код горячий - собирается статистика выполнения и по возможности вставляется if на проверку типа + прямой вызов.

anonymous
()
Ответ на: комментарий от anonymous

Объекты НЕ ХРАНЯТ таблицу виртуальных функций. Хранят только указатель на нее.

Хм. Точно (проверил). Ок, буду знать.

Kroz ★★★★★
()
Ответ на: комментарий от anonymous

Чего, работы jvm? Где тип объекта известен - там вставляется прямой вызов (это и компилятор C++ делает), где неизвестен, а код горячий - собирается статистика выполнения и по возможности вставляется if на проверку типа + прямой вызов.

Ничего не понял.
Кейсы, когда в jvm необходимо было раз-виртуализировать функцию? Не «как», а «в каком случае».

Kroz ★★★★★
()
Ответ на: комментарий от Kroz

Когда у тебя винегрет из виртуальных и не-виртуальных функций...

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

В общем, не убедил.

Да пожалуйста.

Только для меня это звучит так же дико как, например, использовать везде числа с произвольной точностью «просто на всякий случай».

Повторюсь: уж базовый интерфейс можно и осмысленно написать. В наследниках используем override. В итоге код и читать и поддерживать проще. И мы не платим за избавление от детских ошибок. Если уж так их бояться, то С++ стоит вычеркнуть из рассмотрения: всё равно возникать будут, пусть и не в этом месте. Впрочем, способы с ними бороться существуют и проблемы из-за «забытой виртуальности» мне кажутся большой экзотикой. Хотя я, разумеется, предвзят.

Ну и немного офтопа вброшу: мне пришлось по вкусу как в расте динамический полиморфизм сделали - через трейты. В итоге забыть уж точно ничего нельзя: мы всегда явно указываем, что ожидаем «интерфейс» (трейт) или же конкретный тип (без полиморфизма). Но никогда раньше не думал, что плюсовая реализация кому-то проблемной покажется.

DarkEld3r ★★★★★
()
Ответ на: комментарий от DarkEld3r

Только для меня это звучит так же дико как, например, использовать везде числа с произвольной точностью «просто на всякий случай».

У чисел с произвольной точностью проблем намного больше, чем у integer. Уникальность виртуализации всех функций (точнее, моего подхода) - там нет минусов. Звучит дико - да, но это чисто психологический фактор: просто неожиданно и непривычно.

Останемся при своих? Я не против. Есть контр-аргументы к моему подходу - буду рад выслушать.

Kroz ★★★★★
()
Последнее исправление: Kroz (всего исправлений: 1)
Ответ на: комментарий от anonymous

Под Linux-ом очень не странный (Java GUI инородный, mono юзает обертку над GTK2, который морально устарел). Ну можно ещё C выбрать, но легче от этого не станет.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 1)
Ответ на: комментарий от Kroz

На сколько % виртуальная функиция замедляет код?

Смотря насколько он горячий, очевидно. Когда все функции виртуальные... нутыпонел?

anonymous
()
Ответ на: комментарий от Kroz

У чисел с произвольной точностью проблем намного больше, чем у integer.

Так и преимуществ тоже! А удобно-то как. И что характерно, хватает языков использующих такие числа по умолчанию. Ну прямо как с виртуальностью.

Останемся при своих?

Если бы ты (или я) мог поменять мнение, то это уже произошло бы. Ну а так ты игнорируешь аргументы (хотя бы про читабельность/очевидность - если нет virtual/override, то я могу быть уверен, что поведение нигде в наследниках не меняется) и заявляешь, что нет минусов.

И да, потери производительности и памяти есть. Они могут быть незначительными (для конкретного случая или даже вообще), но это не «нет минусов».

DarkEld3r ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.