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

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

Не все упарываются ООП, между прочим. Плюсовые классы - они ддя разного, и во многих случаях виртуальность там как корове седло. STL как пример.

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

Не все упарываются ООП, между прочим.

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

Kroz ★★★★★
()

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

Хреново пишут.

То, что в отсутствие виртуального деструктора у базового класса не будет при определённых обстоятельствах вызван деструктор производного класса, это лишь частности конкретных реализаций. В самом более общем случае, описанном в стандарте, это UB.

Да и вообще, что значит "необходимо деструкторы всех предков делать виртуальными"? К чему такой максимализм? Если архитектура того не требует, то и ладно.

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

Какой ответ ожидается услышать?

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

Вопрос был про конкретные ощутимые минусы, а не про философию.

Без философии, боюсь, не обойтись.

Базовые классы с виртуальными методами пишутся с учетом того, что поведение производного класса в той или иной мере поменяется. Тогда как обычный класс, без виртуальных методов, может рассчитывать на выполнение определенных инвариантов (как раз за счет того, что реализация методов одна и не может быть изменена). Например, std::vector::size() не может возвращать значение, большее, чем значение из std::vector::capacity(). Если методы все методы по-умолчанию виртуальные, то std::vector может столкнуться с тем, что size() внезапно вернет значение, превышающее значение capacity(). Ну и что делать в этом случае?

eao197 ★★★★★
()

а на заборах пишут что Цой жив. виртуальный деструктор нужен, когда есть delete на указателе на предка. если этого по структуре задачи нет), то он ничего не даёт. думать категорией «мудрейшие из мудрейших проффесионалы говорят - надо делать» не стоит. будет возможное проседание по производительности + лишний size_t на инстанс на таблицу виртуальных функций

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

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

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

Ну и что делать в этом случае?

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

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

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

Ну на самом деле, какой профит от unique_ptr, для которого метод release() гарантирует, что оставляет экземпляр unique_ptr пустым.

Я вот не пойму, вы сейчас серьезно, или это такой способ троллинга?

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

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

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

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

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

трейты в плюсах есть: type_traits.

Это не одно и тоже. Ближе всего к аналогу трейтов в Rust (и не только) - абстрактные классы.

anonymous
()

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

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

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

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

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

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

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

Да и вообще, что значит «необходимо деструкторы всех предков делать виртуальными»? К чему такой максимализм? Если архитектура того не требует, то и ладно.

необходимо - в данном случае для уменьшения человеческой ошибки. Цена вроде как не велика (4 байта для указателя на vtable, время на поиск по vtable=переход к нужному оффсету, что довольно быстро), а профит есть (никогда память не потечет).

Архитектура с течением времени подвержена изменениям и порой существенным.

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

Какой ответ ожидается услышать?

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

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

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

Как-то это очень странно звучит. В C++ есть гораздо больше, чем в трейтах раста. А реализация интерфейсов с помощью наследования не влечет никаких таких дополнительных накладных расходов (если не указывать в интерфейсе виртуальный деструктор).

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

В C++ есть гораздо больше, чем в трейтах раста.

Это тоже звучит странно.

(если не указывать в интерфейсе виртуальный деструктор).

Интересное предложение. Ну и других виртуальных функций в интерфейсе тоже не предвидится что ли?

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

Это тоже звучит странно.

Это просто факт. Поддержка ООП в расте чуть менее, чем никакая - всё описание помещается на одной страничке.

Интересное предложение. Ну и других виртуальных функций в интерфейсе тоже не предвидится что ли?

Тут речь про сравнение с другими языками.

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

если не указывать в интерфейсе виртуальный деструктор

я за такое бью железной линейкой по пальцам

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

Это просто факт.

Что факт? В расте нет наследования данных - да и фиг с ним. Плохо разве что отсутствие удобного сахара для делегирования реализации трейтов. В остальном всё необходимое присутствует.

Ну или раскрой мысль чего именно тебе не хватает.

Тут речь про сравнение с другими языками.

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

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

Что факт? В расте нет наследования данных - да и фиг с ним. Плохо разве что отсутствие удобного сахара для делегирования реализации трейтов. В остальном всё необходимое присутствует.

Ну или раскрой мысль чего именно тебе не хватает.

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

В смысле? Если в интерфейсе присутствуют виртуальные функции (а иначе нафига такой интерфейс нужен)

Для статического полиморфизма например.

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

Из необходимого нет даже инкапсуляции.

Есть. Или я чего-то не знаю?

Наследование просто никакое.

Наследования вообще нет.

Полиморфизм - ну, будем считать, что есть.

Статический на голову выше плюсового. Динамический тоже есть.

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

трейты в плюсах есть: type_traits. просто мало кто их применяет и сделано через темплейты.

Оно сделано настолько через одно место, что просто плакать хочется.

Rust:

extern crate num;
use num::Num;

fn only_num<N: Num>(n: N) {
    println!("{:?}", n);
}

fn main() {
    only_num(4);
    only_num("b");
}
src/main.rs:57:5: 57:13 error: the trait bound `&str: num::Num` is not satisfied [E0277]
src/main.rs:57     only_num("b");
                   ^~~~~~~~
src/main.rs:57:5: 57:13 help: run `rustc --explain E0277` to see a detailed explanation
src/main.rs:57:5: 57:13 note: required by `only_num`

C++11:

template<typename N, typename = typename std::enable_if<std::is_arithmetic<N>::value, N>::type>
void only_num(N n)
{
    std::cout << n;
}

int main()
{
    only_num(5);
    only_num("5");
}
main.cpp:44:5: error: no matching function for call to 'only_num'
    only_num("5");
    ^~~~~~~~
main.cpp:31:57: note: candidate template ignored: disabled by 'enable_if' [with N = const char *]
template<typename N, typename = typename std::enable_if<std::is_arithmetic<N>::value, N>::type>

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

Есть. Или я чего-то не знаю?

Не знаешь что такое настоящая инкапсуляция, видимо)

Статический на голову выше плюсового. Динамический тоже есть.

На голову выше чего? Плюсовых шаблонов? В языке без нормальной инкапсуляции и наследования?

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

Не знаешь что такое настоящая инкапсуляция, видимо)

Очень аргументированно.

В языке без нормальной инкапсуляции и наследования?

Представьте себе.

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

Похоже, и вы, и Iron_Bug по поводу трейтов в C++ придумали что-то свое и это свое пытаетесь противопоставлять трейтам из Rust-а.

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

Трейт здесь только is_arithmetic. Использование enable_if для перегрузки в C++, имхо, не есть аналогия с трейтами из Rust-а.

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

Думал это очевидно. Функция должна принимать только числа.

Тогда еще раз: почему вы решили, что это имеет отношение к трайтам в C++?

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

Да чё вы прикопались до меня?

Да хочу понять, вы действительно слабо разбраетесь в C++ или это только кажется. Складывается впечатление, что в Rust-е разбираетесь гораздо лучше. А вот про трайты типов в C++ только слышали. Отсюда и попытка соотнести трайты Rust-а и SFINAE с использованием трайтов в C++.

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

Под Linux-ом очень не странный

python+gtk

vala

haskell+gtk

qml

javascript+gtk

куча вариантов язычков, на которых можно делать gui+io без мороки.

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

Хз что я лучше знаю. На плюсах я пишу 5 лет, а на Rust 2 месяца.

То, что SFINAE != трейты раста я в курсе. Это был пример того, насколько проще в rust решить данную задачу.

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

Увы, но GTK+ существует только на лине. Поэтому не вариант.

RazrFalcon ★★★★★
()

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

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

То, что SFINAE != трейты раста я в курсе.

Тогда не понятно, зачем было такой пример приводить. Речь же шла о том, как трайты в C++ позволяют обходиться без наследования классов с виртуальными методами. Тут нужно говорить о чем-то вроде memory_occupation из этого примера: http://cpp.sh/82yh

Хотя, применительно к наследованию с изменением поведения без виртуальных методов, нужно смотреть в сторону policy-based design и CRTP.

PS. trait-ы и policy в C++ легко спутать (что, полагаю, и произошло в случае с Iron_Bug). Вот здесь есть толковые объяснения похожести и различий.

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

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

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

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

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

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

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

Все-таки лучше говорить «скорее всего» или «желательно», нежели «должен».

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

То, что SFINAE != трейты раста я в курсе. Это был пример того, насколько проще в rust решить данную задачу.

Ждем концептов. В gcc 6 уже есть реализация:

~$ cat ./test.cpp
#include <type_traits>
using namespace std;

template <class T>
concept bool IntegralSigned() { 
    return is_integral<T>::value && is_signed<T>::value; 
}


void foo( IntegralSigned& v ) {
    --v;
}


int main() {
    int    a { 0 };
    size_t b { 0 };
    
    foo( a );
    foo( b );
}

~$ g++ -fconcepts ./test.cpp 
./test.cpp: In function ‘int main()’:
./test.cpp:21:12: error: cannot call function ‘void foo(auto:1&) [with auto:1 = long unsigned int]’
     foo( b );
            ^
./test.cpp:11:6: note:   constraints not satisfied
 void foo( IntegralSigned& v ) {
      ^~~
./test.cpp:6:14: note: within ‘template<class T> concept bool IntegralSigned() [with T = long unsigned int]’
 concept bool IntegralSigned() {
              ^~~~~~~~~~~~~~
./test.cpp:6:14: note: ‘std::is_signed<long unsigned int>::value’ evaluated to false
anonymous
()
Ответ на: комментарий от RazrFalcon

Жесть. Бедный С++.

Странная реакция. Это просто сахар к шаблонам, чтоб писать меньше кода и получать более внятные ошибки. Вот, например, напиши на расте функцию, чтоб она принимала массивы размерностью до 256 элементов и у которых тип элементов - любое число со знаком (int, double etc.). С концептами это запишется так:

template<class T>
concept bool SmallSignedArray() { 
    return 
        Array<T> && 
        SignedArithmetic<typename remove_extent<T>::type> &&
        extent<T>::value < 256;
}

void sort_small_array( SmallSignedArray& a ) {
}


int main() {
    int a[ 100 ];
    sort_small_array( a );    
}
anonymous
()
Ответ на: комментарий от anonymous

Это реакция не переусложнение и так сложного языка.

Я не очень хорошо разбираюсь в шаблонах раста, чтобы написать аналог.

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

Это реакция не переусложнение и так сложного языка.

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

Я не очень хорошо разбираюсь в шаблонах раста, чтобы написать аналог.

Я тоже не разбираюсь в нем хорошо, но сдается мне - там это в принципе невозможно.

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