RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 10:23
Оценка:
Привет,

давно не писал на плюсах, вот вернулся снова и понеслась душа в пляс

Надоели всякие 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);
   }
}
Re: RAII и исключения в конструкторе
От: PlushBeaver  
Дата: 04.07.20 10:53
Оценка: +4
Здравствуйте, C0x, Вы писали:

C0x>Но как вам решение проблемы деструкции данных класса через базовый класс? Т.е. пихаем все ресурсные поля класса в базовый класс, который умеет их зачищать в деструкторе. После того как в нашем классе в конструкторе вылетает исключение, в любом случае вызовится деструктор базового класса и он освободит ресурсы. Есть такой подход в Си++ мире или лучше так не делать?


Сложно --- наследование и шаблоны на ровном месте, ненадежно --- можно забыть освободить ресурс или освободить не в том порядке. Пусть каждый ресурс будет отдельным объектом с RAII в составе A. При вылете исключения из конструктора A для тех из них, которые успели сконструироваться, будут вызваны деструкторы.
Re: RAII и исключения в конструкторе
От: Basil2 Россия https://starostin.msk.ru
Дата: 04.07.20 10:54
Оценка: +3
Здравствуйте, C0x, Вы писали:

C0x>Но как вам решение проблемы деструкции данных класса через базовый класс?


Креативно.

Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:

struct A
{
private:
  smart_resource_ptr _someHandle1 = LoadHeavyResource1(); //Тут может быть исключение
  smart_resource_ptr _someHandle2 = LoadHeavyResource2(); //Тут может быть исключение
  smart_resource_ptr _someHandle3 = LoadHeavyResource3(); //Тут может быть исключение
}
Проект Ребенок8020 — пошаговый гайд как сделать, вырастить и воспитать ребенка.
Re: RAII и исключения в конструкторе
От: velkin Удмуртия http://blogs.rsdn.org/effective/
Дата: 04.07.20 11:32
Оценка: 6 (1) +2
Здравствуйте, C0x, Вы писали:

C0x>Но как вам решение проблемы деструкции данных класса через базовый класс?


В Qt есть базовый класс, называется QObject. Наследуешься от него естественно не забывая про метаобъектный компилятор, а когда создаёшь объект в куче присваиваешь родителя. Удалишь родительский объект, удалишь и дочерние объекты.

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

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

C0x>Со своей стороны я вижу очевидный плюс такого подхода — нет лишних методов типа Release, Close и т.д.


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

Автоматическое освобождение любых ресурсов не проблема, просто в C++ не принято решать за программиста такие вопросы, он должен сам выбрать нужно ему это или нет для оптимальной производительности.
Re[2]: RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 12:00
Оценка:
Здравствуйте, velkin, Вы писали:

V>Здравствуйте, C0x, Вы писали:


C0x>>Но как вам решение проблемы деструкции данных класса через базовый класс?


V>В Qt есть базовый класс, называется QObject. Наследуешься от него естественно не забывая про метаобъектный компилятор, а когда создаёшь объект в куче присваиваешь родителя. Удалишь родительский объект, удалишь и дочерние объекты.

V>Другой вариант использовать умный указатель, но это везде в C++ и не по одной реализации в каждой библиотеке. Есть различные виды умных указателей. Классический, как только пропадают все ссылки на объект в куче, так он автоматически уничтожается.

Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.

C0x>>Со своей стороны я вижу очевидный плюс такого подхода — нет лишних методов типа Release, Close и т.д.


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

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

Вот я сейчас как раз поэтому задался этим вопросом, потому-что создаю библиотеку которая работает с кучей видео-аудио потоков. Мне было очевидно использовать Open, Close, потому-что это сходит из самых низов — Си, открыть, закрыть в нужный момент. Но я сейчас смотрю на это все и понимаю, а ведь есть такая штука как RAII, и будет дурно не использовать эту возможность которую дает тебе сам язык. Пробую теперь обойтись без Open/Close, возможно и не получится. Но в любом случае хочется построить красивый стройный код, без необходимости (ой, забыл вызвать Close перед этим другим Close).
Re[3]: RAII и исключения в конструкторе
От: watchmaker  
Дата: 04.07.20 12:09
Оценка: +5
Здравствуйте, C0x, Вы писали:

C0x>Здравствуйте, velkin, Вы писали:


V>>Другой вариант использовать умный указатель, но это везде в C++ и не по одной реализации в каждой библиотеке. Есть различные виды умных указателей. Классический, как только пропадают все ссылки на объект в куче, так он автоматически уничтожается.


C0x>Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.

Какой нетривиальной операцией и какую логику? У тебя в пример вызывается просто ReleaseHeavyResource1. Это обычным std::unique_ptr делается:
std::unique_ptr<HANDLE, ReleaseHeavyResource1Deleter>

Ну и аналогично со всеми другими умными указателями.

Просто используешь в своём классе не голый HANDLE, а этот тип: std::unique_ptr<HANDLE, ReleaseHeavyResource1Deleter> _someHandle1 = NULL; — и его ресурсы будут освобождены как при нормальном разрушении объекта, так и при любых исключениях в конструкторе или в другом месте.
Re[2]: RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 12:11
Оценка:
Здравствуйте, Basil2, Вы писали:

B>Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:


В простых случаях да, это поможет. Но если нужна сложная логика освобождения? Я видел какие-то велосипеды с shared_ptr куда пихается лямбда функция с какой-то логикой, но это как-то помоему велосипедно слишком выглядит и не похоже на какой-то общий паттерн, который можно по всей либе юзать.
Re[4]: RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 12:13
Оценка: -1
Здравствуйте, watchmaker, Вы писали:

W>Здравствуйте, C0x, Вы писали:


W>Просто используешь в своём классе не голый HANDLE, а этот тип: std::unique_ptr<HANDLE, ReleaseHeavyResource1Deleter> _someHandle1 = NULL; — и его ресурсы будут освобождены как при нормальном разрушении объекта, так и при любых исключениях в конструкторе или в другом месте.


Хм, спасибо, видал уже где-то такое, можно еще и лямбду в птр зафигачить при желании. Но как-то это страшно все потом выглядит. На лапшу похоже.
Re: RAII и исключения в конструкторе
От: T4r4sB Россия  
Дата: 04.07.20 12:16
Оценка: +1
Здравствуйте, 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> }


Каждая пара LoadHeavyResource1()-ReleaseHeavyResource1 должна быть в отдельном объекте.
Один объект — один ресурс.
Re[2]: RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 12:17
Оценка:
Здравствуйте, PlushBeaver, Вы писали:

PB>Здравствуйте, C0x, Вы писали:


C0x>>Но как вам решение проблемы деструкции данных класса через базовый класс? Т.е. пихаем все ресурсные поля класса в базовый класс, который умеет их зачищать в деструкторе. После того как в нашем классе в конструкторе вылетает исключение, в любом случае вызовится деструктор базового класса и он освободит ресурсы. Есть такой подход в Си++ мире или лучше так не делать?


PB>Сложно --- наследование и шаблоны на ровном месте, ненадежно --- можно забыть освободить ресурс или освободить не в том порядке. Пусть каждый ресурс будет отдельным объектом с RAII в составе A.


Мне вот это почему-то тоже кажется сложным. У меня есть типы типа HANDLE, оборачивать их в спец объект ради одной простой цели — надежного уничтожения везде и всегда кажется черезчурным. Да и хочется не с врапперами в коде работать а с исходными типами (видить их в полях класса и возращаемых зачениях функций). Смартпоинтеры на мой личный взгляд это в целом костыль, который превращает код в лапшу.
Re[2]: RAII и исключения в конструкторе
От: C0x  
Дата: 04.07.20 12:22
Оценка: -4
Здравствуйте, 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# для управления освобождением ресурсов.
Re[3]: RAII и исключения в конструкторе
От: T4r4sB Россия  
Дата: 04.07.20 12:53
Оценка:
Здравствуйте, C0x, Вы писали:

C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем.


Вообще-то это и есть использование RAII по назначению. Только не смартпоинтер, конечно. А именно HWND_Holder, DC_Holder итд
Re: RAII и исключения в конструкторе
От: σ  
Дата: 04.07.20 14:03
Оценка: 18 (5) +1
Не нужно возиться с конструкторами базовых классов, нужно сделать конструктор с `LoadHeavyResource1` delegating-конструктором
См. https://youtu.be/uQyT-5iWUow?t=3147, How to handle constructors that must acquire multiple resources in an exception safe manner
Отредактировано 04.07.2020 14:43 σ . Предыдущая версия . Еще …
Отредактировано 04.07.2020 14:43 σ . Предыдущая версия .
Re[3]: RAII и исключения в конструкторе
От: _NN_ www.nemerleweb.com
Дата: 04.07.20 15:06
Оценка:
Здравствуйте, C0x, Вы писали:

C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем. И такие костыли вызывают у программистов на Си#/Java насмешки типа "Да у вас там в Си++ всё на костылях и костылями погоняете". А вот моё решении, на мой опять же взгляд, более похоже на четкий паттерн для решения задачи освобождения ресурсов заданного класса на базе языковой конструкции и порядка вызова деструкторов. И вот это мне кажется более аккуратным способом, чем тот же "костыль" IDisposable в C# для управления освобождением ресурсов.


В C# давно есть SafeHandle и CriticalFinalizerObject, которые решают задачу освобождения ресурсов получше ручного try-finally.
http://rsdn.nemerleweb.com
http://nemerleweb.com
Re[3]: RAII и исключения в конструкторе
От: a7d3  
Дата: 04.07.20 15:23
Оценка:
Здравствуйте, C0x, Вы писали:

C0x>Здравствуйте, Basil2, Вы писали:


B>>Но обычно в таком случае делается класс-обертка для конкретного типа ресурсов. Тогда весь код выглядит так:


C0x>В простых случаях да, это поможет. Но если нужна сложная логика освобождения? Я видел какие-то велосипеды с shared_ptr куда пихается лямбда функция с какой-то логикой, но это как-то помоему велосипедно слишком выглядит и не похоже на какой-то общий паттерн, который можно по всей либе юзать.


А какая разница вызывается эта сложная логика после того как освобождаемый ресурс стал не нужен или же в случае раскрутки стека из-за исключения?
Просто не забывать про SOLID и делать классы максимально простыми — первая же буква S.
Каждому типу ресурса соответствует один маленький класс-обёртка над ним, в том случае, т.к. логика освобождения ресурсов различается по их типам.
Re[3]: RAII и исключения в конструкторе
От: a7d3  
Дата: 04.07.20 15:30
Оценка:
Здравствуйте, C0x, Вы писали:

C0x>Здравствуйте, T4r4sB, Вы писали:


TB>>Каждая пара LoadHeavyResource1()-ReleaseHeavyResource1 должна быть в отдельном объекте.

TB>>Один объект — один ресурс.

C0x>Попробую объяснить свою думку. Мне использование спец. объектов (те же смартпоинтеры) для уничтожения кажется большим костылем. И такие костыли вызывают у программистов на Си#/Java насмешки типа "Да у вас там в Си++ всё на костылях и костылями погоняете". А вот моё решении, на мой опять же взгляд, более похоже на четкий паттерн для решения задачи освобождения ресурсов заданного класса на базе языковой конструкции и порядка вызова деструкторов. И вот это мне кажется более аккуратным способом, чем тот же "костыль" IDisposable в C# для управления освобождением ресурсов.


Базовые классы и наследование от них — это инструмент взаимозаменяемости. Когда клиент класса освобождается от знания о том, какой же на самом деле тип ему был передан для использования.
Все другие варианты использования наследования — типа ради попыток запихнуть в базовый класс повторяющуюся функциональность — это от лукавого — по канонам ООП должно делаться через агрегированием с делегированием внешних вызовов.
Причин тому миллион.
Re: RAII и исключения в конструкторе
От: Reset  
Дата: 05.07.20 03:32
Оценка: +1
Еще есть делегирующие конструкторы.

P.S. Но про них уже написали...
Re[3]: RAII и исключения в конструкторе
От: velkin Удмуртия http://blogs.rsdn.org/effective/
Дата: 05.07.20 03:48
Оценка:
Здравствуйте, C0x, Вы писали:

C0x>Я имел ввиду проблемы деструкции в момент исключения в конструкторах. Удаление может быть не тривиальной операцией и содержать логику. Простыми умными указателями тут не обойтись. Но мне нужно гарантировать что уничтожение пройдет правильно в любом случае даже если объект не до конца создан.


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

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

Я уж забыл из какого это источника, но там говорилось что-то вроде того, что помните, конструктор и деструктор это методы (функции-члены). Это вроде бы кажется очевидным, но люди об этом забывают, когда создают классы. С тем же успехом можно было написать свою собственную реалиацию, ну то есть void my_new() или void my_delete(), а потом при создании объекта вызвать их вручную.

В каком-то смысле все эти конструкции языка C++ не более, чем синтаксический сахар. Точно так же как другие операторы, ведь как бы они не выглядели это просто функции члены или друзья. Важно ведь не то, что конкретно написано в данном случае, важно то, что программист хочет этим выразить.
Re: RAII и исключения в конструкторе
От: Мирный герцог Ниоткуда  
Дата: 05.07.20 06:49
Оценка: +2
Здравствуйте, C0x, Вы писали:

C0x>Привет,


привет, подобная машинерия лишь усложняет код на ровном месте, и то что автору кажется "удобным" становится головной болью для мэйнтейнеров кода. Как ad-hoc решение используется тайпдеф а std::unique_ptr с кастомным делетером, о чём уже упомянуто выше, или нужно не полениться и написать таки враппер для конкретного типа хендла.
нормально делай — нормально будет
Re[4]: RAII и исключения в конструкторе
От: C0x  
Дата: 05.07.20 08:27
Оценка:
Здравствуйте, velkin, Вы писали:

V>Здравствуйте, C0x, Вы писали:




V>В каком-то смысле все эти конструкции языка C++ не более, чем синтаксический сахар. Точно так же как другие операторы, ведь как бы они не выглядели это просто функции члены или друзья. Важно ведь не то, что конкретно написано в данном случае, важно то, что программист хочет этим выразить.


На самом деле все не совсем так. Я стараюсь не использовать new и deleted в своих программах, а создавать объекты на стеке. Также я стараюсь писать логику программы зная один очень полезный факт из мира Си, объекты на стеке удаляюься сразу же по выходу из блока кода, а это значит автоматический вызов деструктора. Например в языке Go ввели специальную конструкцию derer для того чтобы что-то сделать по выходу из блока и в нужном порядке, а тут бесплатно.
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.