давно не писал на плюсах, вот вернулся снова и понеслась душа в пляс
Надоели всякие Begin, End, Open, Close методы в программах. Хочу придерживаться принципа RAII.
Но возникает классическая проблема вылетает исключение в конструкторе и пипец. Ну скажем я Handle какой-нибудь на ресурс получил, а после исключения естественно деструктор не вызывается и ресурс не освобождается. Ну как бы, да, есть ломовое решение перехватывать закрыть конструктор в try catch и освобождать ресурсы через метод Release(), которой так же дергается и в самом деструкторе нашего класса.
Но как вам решение проблемы деструкции данных класса через базовый класс? Т.е. пихаем все ресурсные поля класса в базовый класс, который умеет их зачищать в деструкторе. После того как в нашем классе в конструкторе вылетает исключение, в любом случае вызовится деструктор базового класса и он освободит ресурсы. Есть такой подход в Си++ мире или лучше так не делать?
Со своей стороны я вижу очевидный плюс такого подхода — нет лишних методов типа Release, Close и т.д. Все делается средствами самого языка: конструктор-деструктор. Как бы чисто всё чтоли.
Пример:
template<typename TData>
struct A : public TData
{
A()
{
_someHandle1 = LoadHeavyResource1(); //Тут может быть исключение
_someHandle2 = LoadHeavyResource2(); //Тут может быть исключение
_someHandle3 = LoadHeavyResource3(); //Тут может быть исключение
}
}
struct A_Data
{
HANDLE _someHandle1 = NULL;
HANDLE _someHandle2 = NULL;
HANDLE _someHandle3 = NULL;
~A_Data()
{
if (_someHandle1 != NULL) ReleaseHeavyResource1(_someHandle1);
if (_someHandle2 != NULL) ReleaseHeavyResource2(_someHandle2);
if (_someHandle3 != NULL) ReleaseHeavyResource3(_someHandle3);
}
}
Здравствуйте, C0x, Вы писали:
C0x>Но как вам решение проблемы деструкции данных класса через базовый класс? Т.е. пихаем все ресурсные поля класса в базовый класс, который умеет их зачищать в деструкторе. После того как в нашем классе в конструкторе вылетает исключение, в любом случае вызовится деструктор базового класса и он освободит ресурсы. Есть такой подход в Си++ мире или лучше так не делать?
Сложно --- наследование и шаблоны на ровном месте, ненадежно --- можно забыть освободить ресурс или освободить не в том порядке. Пусть каждый ресурс будет отдельным объектом с RAII в составе A. При вылете исключения из конструктора A для тех из них, которые успели сконструироваться, будут вызваны деструкторы.
Здравствуйте, C0x, Вы писали:
C0x>Но как вам решение проблемы деструкции данных класса через базовый класс?
Креативно.
Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:
struct A
{
private:
smart_resource_ptr _someHandle1 = LoadHeavyResource1(); //Тут может быть исключение
smart_resource_ptr _someHandle2 = LoadHeavyResource2(); //Тут может быть исключение
smart_resource_ptr _someHandle3 = LoadHeavyResource3(); //Тут может быть исключение
}
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Здравствуйте, C0x, Вы писали:
C0x>Но как вам решение проблемы деструкции данных класса через базовый класс?
В Qt есть базовый класс, называется QObject. Наследуешься от него естественно не забывая про метаобъектный компилятор, а когда создаёшь объект в куче присваиваешь родителя. Удалишь родительский объект, удалишь и дочерние объекты.
Другой вариант использовать умный указатель, но это везде в C++ и не по одной реализации в каждой библиотеке. Есть различные виды умных указателей. Классический, как только пропадают все ссылки на объект в куче, так он автоматически уничтожается.
Порядок уничтожения между этими способами разный, но результат автоматическое уничтожение без вызова delete.
C0x>Со своей стороны я вижу очевидный плюс такого подхода — нет лишних методов типа Release, Close и т.д.
А вот это я думаю вовсе не лишние методы, а так специально спроектированная система. Если сделали методы вроде открыть, закрыть, например, для файла или соединения с базой данных, то это как бы разработчики непрозрачно намекают, чтобы программист управлял вручную всем этим хозяйством пока существует объект с этими методами.
Автоматическое освобождение любых ресурсов не проблема, просто в C++ не принято решать за программиста такие вопросы, он должен сам выбрать нужно ему это или нет для оптимальной производительности.
Здравствуйте, velkin, Вы писали:
V>Здравствуйте, C0x, Вы писали:
C0x>>Но как вам решение проблемы деструкции данных класса через базовый класс?
V>В Qt есть базовый класс, называется QObject. Наследуешься от него естественно не забывая про метаобъектный компилятор, а когда создаёшь объект в куче присваиваешь родителя. Удалишь родительский объект, удалишь и дочерние объекты. V>Другой вариант использовать умный указатель, но это везде в C++ и не по одной реализации в каждой библиотеке. Есть различные виды умных указателей. Классический, как только пропадают все ссылки на объект в куче, так он автоматически уничтожается.
Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.
C0x>>Со своей стороны я вижу очевидный плюс такого подхода — нет лишних методов типа Release, Close и т.д.
V>А вот это я думаю вовсе не лишние методы, а так специально спроектированная система. Если сделали методы вроде открыть, закрыть, например, для файла или соединения с базой данных, то это как бы разработчики непрозрачно намекают, чтобы программист управлял вручную всем этим хозяйством пока существует объект с этими методами. V>Автоматическое освобождение любых ресурсов не проблема, просто в C++ не принято решать за программиста такие вопросы, он должен сам выбрать нужно ему это или нет для оптимальной производительности.
Вот я сейчас как раз поэтому задался этим вопросом, потому-что создаю библиотеку которая работает с кучей видео-аудио потоков. Мне было очевидно использовать Open, Close, потому-что это сходит из самых низов — Си, открыть, закрыть в нужный момент. Но я сейчас смотрю на это все и понимаю, а ведь есть такая штука как RAII, и будет дурно не использовать эту возможность которую дает тебе сам язык. Пробую теперь обойтись без Open/Close, возможно и не получится. Но в любом случае хочется построить красивый стройный код, без необходимости (ой, забыл вызвать Close перед этим другим Close).
Здравствуйте, C0x, Вы писали:
C0x>Здравствуйте, velkin, Вы писали:
V>>Другой вариант использовать умный указатель, но это везде в C++ и не по одной реализации в каждой библиотеке. Есть различные виды умных указателей. Классический, как только пропадают все ссылки на объект в куче, так он автоматически уничтожается.
C0x>Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.
Какой нетривиальной операцией и какую логику? У тебя в пример вызывается просто ReleaseHeavyResource1. Это обычным std::unique_ptr делается:
Ну и аналогично со всеми другими умными указателями.
Просто используешь в своём классе не голый HANDLE, а этот тип: std::unique_ptr<HANDLE, ReleaseHeavyResource1Deleter> _someHandle1 = NULL; — и его ресурсы будут освобождены как при нормальном разрушении объекта, так и при любых исключениях в конструкторе или в другом месте.
Здравствуйте, Basil2, Вы писали:
B>Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:
В простых случаях да, это поможет. Но если нужна сложная логика освобождения? Я видел какие-то велосипеды с shared_ptr куда пихается лямбда функция с какой-то логикой, но это как-то помоему велосипедно слишком выглядит и не похоже на какой-то общий паттерн, который можно по всей либе юзать.
Здравствуйте, watchmaker, Вы писали:
W>Здравствуйте, C0x, Вы писали:
W>Просто используешь в своём классе не голый HANDLE, а этот тип: std::unique_ptr<HANDLE, ReleaseHeavyResource1Deleter> _someHandle1 = NULL; — и его ресурсы будут освобождены как при нормальном разрушении объекта, так и при любых исключениях в конструкторе или в другом месте.
Хм, спасибо, видал уже где-то такое, можно еще и лямбду в птр зафигачить при желании. Но как-то это страшно все потом выглядит. На лапшу похоже.
Здравствуйте, PlushBeaver, Вы писали:
PB>Здравствуйте, C0x, Вы писали:
C0x>>Но как вам решение проблемы деструкции данных класса через базовый класс? Т.е. пихаем все ресурсные поля класса в базовый класс, который умеет их зачищать в деструкторе. После того как в нашем классе в конструкторе вылетает исключение, в любом случае вызовится деструктор базового класса и он освободит ресурсы. Есть такой подход в Си++ мире или лучше так не делать?
PB>Сложно --- наследование и шаблоны на ровном месте, ненадежно --- можно забыть освободить ресурс или освободить не в том порядке. Пусть каждый ресурс будет отдельным объектом с RAII в составе A.
Мне вот это почему-то тоже кажется сложным. У меня есть типы типа HANDLE, оборачивать их в спец объект ради одной простой цели — надежного уничтожения везде и всегда кажется черезчурным. Да и хочется не с врапперами в коде работать а с исходными типами (видить их в полях класса и возращаемых зачениях функций). Смартпоинтеры на мой личный взгляд это в целом костыль, который превращает код в лапшу.
Здравствуйте, T4r4sB, Вы писали:
TB>Здравствуйте, C0x, Вы писали:
C0x>> A() C0x>> { C0x>> _someHandle1 = LoadHeavyResource1(); //Тут может быть исключение C0x>> _someHandle2 = LoadHeavyResource2(); //Тут может быть исключение C0x>> _someHandle3 = LoadHeavyResource3(); //Тут может быть исключение C0x>> }
C0x>> ~A_Data() C0x>> { C0x>> if (_someHandle1 != NULL) ReleaseHeavyResource1(_someHandle1); C0x>> if (_someHandle2 != NULL) ReleaseHeavyResource2(_someHandle2); C0x>> if (_someHandle3 != NULL) ReleaseHeavyResource3(_someHandle3); C0x>> }
TB>Каждая пара LoadHeavyResource1()-ReleaseHeavyResource1 должна быть в отдельном объекте. TB>Один объект — один ресурс.
Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем. И такие костыли вызывают у программистов на Си#/Java насмешки типа "Да у вас там в Си++ всё на костылях и костылями погоняете". А вот моё решении, на мой опять же взгляд, более похоже на четкий паттерн для решения задачи освобождения ресурсов заданного класса на базе языковой конструкции и порядка вызова деструкторов. И вот это мне кажется более аккуратным способом, чем тот же "костыль" IDisposable в C# для управления освобождением ресурсов.
Здравствуйте, C0x, Вы писали:
C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем.
Вообще-то это и есть использование RAII по назначению. Только не смартпоинтер, конечно. А именно HWND_Holder, DC_Holder итд
Здравствуйте, C0x, Вы писали:
C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем. И такие костыли вызывают у программистов на Си#/Java насмешки типа "Да у вас там в Си++ всё на костылях и костылями погоняете". А вот моё решении, на мой опять же взгляд, более похоже на четкий паттерн для решения задачи освобождения ресурсов заданного класса на базе языковой конструкции и порядка вызова деструкторов. И вот это мне кажется более аккуратным способом, чем тот же "костыль" IDisposable в C# для управления освобождением ресурсов.
Здравствуйте, C0x, Вы писали:
C0x>Здравствуйте, Basil2, Вы писали:
B>>Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:
C0x>В простых случаях да, это поможет. Но если нужна сложная логика освобождения? Я видел какие-то велосипеды с shared_ptr куда пихается лямбда функция с какой-то логикой, но это как-то помоему велосипедно слишком выглядит и не похоже на какой-то общий паттерн, который можно по всей либе юзать.
А какая разница вызывается эта сложная логика после того как освобождаемый ресурс стал не нужен или же в случае раскрутки стека из-за исключения?
Просто не забывать про SOLID и делать классы максимально простыми — первая же буква S.
Каждому типу ресурса соответствует один маленький класс-обёртка над ним, в том случае, т.к. логика освобождения ресурсов различается по их типам.
Здравствуйте, C0x, Вы писали:
C0x>Здравствуйте, T4r4sB, Вы писали:
TB>>Каждая пара LoadHeavyResource1()-ReleaseHeavyResource1 должна быть в отдельном объекте. TB>>Один объект — один ресурс.
C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем. И такие костыли вызывают у программистов на Си#/Java насмешки типа "Да у вас там в Си++ всё на костылях и костылями погоняете". А вот моё решении, на мой опять же взгляд, более похоже на четкий паттерн для решения задачи освобождения ресурсов заданного класса на базе языковой конструкции и порядка вызова деструкторов. И вот это мне кажется более аккуратным способом, чем тот же "костыль" IDisposable в C# для управления освобождением ресурсов.
Базовые классы и наследование от них — это инструмент взаимозаменяемости. Когда клиент класса освобождается от знания о том, какой же на самом деле тип ему был передан для использования.
Все другие варианты использования наследования — типа ради попыток запихнуть в базовый класс повторяющуюся функциональность — это от лукавого — по канонам ООП должно делаться через агрегированием с делегированием внешних вызовов.
Причин тому миллион.
Здравствуйте, C0x, Вы писали:
C0x>Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.
Умный указатель всего лишь решает проблему автоматического удаления объекта, как вариант, когда на него больше никто не ссылается. А конструктор и деструктор это методы, или как в C++ принято функции-члены. В них можно поместить любую логику, или просто вызов функций отвечающих за эту логику.
То есть new в случае указателей и умных указателей запустит один из конструкторов, а delete запустит деструктор, но в случае с умными указателями последний может вызваться так сказать автоматически согласно запрограммированной логике.
Я уж забыл из какого это источника, но там говорилось что-то вроде того, что помните, конструктор и деструктор это методы (функции-члены). Это вроде бы кажется очевидным, но люди об этом забывают, когда создают классы. С тем же успехом можно было написать свою собственную реалиацию, ну то есть void my_new() или void my_delete(), а потом при создании объекта вызвать их вручную.
В каком-то смысле все эти конструкции языка C++ не более, чем синтаксический сахар. Точно так же как другие операторы, ведь как бы они не выглядели это просто функции члены или друзья. Важно ведь не то, что конкретно написано в данном случае, важно то, что программист хочет этим выразить.
привет, подобная машинерия лишь усложняет код на ровном месте, и то что автору кажется "удобным" становится головной болью для мэйнтейнеров кода. Как ad-hoc решение используется тайпдеф а std::unique_ptr с кастомным делетером, о чём уже упомянуто выше, или нужно не полениться и написать таки враппер для конкретного типа хендла.
Здравствуйте, velkin, Вы писали:
V>Здравствуйте, C0x, Вы писали:
V>В каком-то смысле все эти конструкции языка C++ не более, чем синтаксический сахар. Точно так же как другие операторы, ведь как бы они не выглядели это просто функции члены или друзья. Важно ведь не то, что конкретно написано в данном случае, важно то, что программист хочет этим выразить.
На самом деле все не совсем так. Я стараюсь не использовать new и deleted в своих программах, а создавать объекты на стеке. Также я стараюсь писать логику программы зная один очень полезный факт из мира Си, объекты на стеке удаляюься сразу же по выходу из блока кода, а это значит автоматический вызов деструктора. Например в языке Go ввели специальную конструкцию derer для того чтобы что-то сделать по выходу из блока и в нужном порядке, а тут бесплатно.