Много лет не могу для себя решить, как правильно себя вести при возникновении каких-то проблем во время выполнения деструктора.
Например, для конкретики, есть у нас класс, в конструкторе читает данные из файла, в деструкторе — записывает модифицированные данные обратно в файл. Делать все это хочется именно в конструкторе и деструкторе, чтобы эти действия выполнялись автоматически, при возникновении исключений при выполнении промежуточных действий — они правильно обрабатывались и т.п., как мне кажется, это стандартная более-менее практика.
И вот что делать, если в деструкторе не удалось выполнить запись в файл? Исключение кидать нельзя, игнорировать ошибку как-то неправильно, запись в лог — какое-то костыльное решение, не всегда уместно.
Не выполнять в деструкторе никаких сложных действий? Но как же тогда концепция автоматического захвата-освобождения ресурсов в конструкторе-деструкторе, она вроде как считается правильной, и мой пример работы с файлом в нее укладывается, насколько я ее понимаю.
В общем, буду признателен, если вы поделитесь своими способами реализовать описанный в примере сценарий работы идеологически правильным способом.)
Здравствуйте, jahr, Вы писали:
J>Много лет не могу для себя решить, как правильно себя вести при возникновении каких-то проблем во время выполнения деструктора.
Конструктор, предназначен для инициализации полей, деструктор, для освобождения ресурсов.
Никаких тяжёлых операций, которые могут пройти неудачно, в них быть не должно. С++ не всесилен.
При необходимости производить тяжёлые операции при инициализации и деинициализации, нужно
делать всё по старинке как в Cи, разруливать в специализированных функциях: object.initialize(), object.deinitialize()
Здравствуйте, jahr, Вы писали:
J>Много лет не могу для себя решить, как правильно себя вести при возникновении каких-то проблем во время выполнения деструктора.
ИМХО ответ на этот вопрос лежит в самом определении деструктора.
Деструктор вызывается при разрушении объекта. То есть, что бы не случилось в Вега деструкторе, остается в деструкторе. И вызываемый код doesn't care.
Отсюда и плясать.
J>И вот что делать, если в деструкторе не удалось выполнить запись в файл? Исключение кидать нельзя, игнорировать ошибку как-то неправильно, запись в лог — какое-то костыльное решение, не всегда уместно.
Всегда уместных решений особо и не бывает.
J>Не выполнять в деструкторе никаких сложных действий? Но как же тогда концепция автоматического захвата-освобождения ресурсов в конструкторе-деструкторе, она вроде как считается правильной, и мой пример работы с файлом в нее укладывается, насколько я ее понимаю.
"Запись в файл" — это не "освобождение ресурса". Это и правда весьма сложное действие, которое может обломаться. В зависимости от стратегии обработки ошбиок, внешней логике на это может быть пофигу, тогда в принципе это можно и в деструкторе делать.
А может и не пофигу, тогда подобному действию не место в деструкторе.
Здравствуйте, jahr, Вы писали:
J>В общем, буду признателен, если вы поделитесь своими способами реализовать описанный в примере сценарий работы идеологически правильным способом.)
Обычно вручную расставляют .close() прям перед разрушением:
{
File a,b;
// ... - may throw
b.close(); // may throw
a.close(); // may throw
}
{
File a,b;
scope(success)
{
b.close(); // may throw
a.close(); // may throw
};
// ... - may throw
}
В этом случае не надо расставлять .close() в каждой точке выхода — return, break, etc — достаточно один раз и рядом с объектом.
Экспериментальная альтернатива — двухфазная семантика разрушения. Вместо деструктора пишутся два метода release и deferred — release выполняется всегда в самом конце, deferred выполняется перед release и только в случае если не летит исключение. В итоге пример выше переписывается в:
Здравствуйте, jahr, Вы писали:
J>Много лет не могу для себя решить, как правильно себя вести при возникновении каких-то проблем во время выполнения деструктора. J>Например, для конкретики, есть у нас класс, в конструкторе читает данные из файла, в деструкторе — записывает модифицированные данные обратно в файл.
Запись в файл делай в отдельном методе Save(), или Flush(), или Close(), etc. В коде явно вызывай этот метод (потенциально бросающий исключение) в нормальном потоке выполнения, не полагаясь на деструктор.
В деструкторе тоже можешь попытаться вызвать этот метод или его содержимое (проверив, что объект «грязный» и сохранение действительно нужно, и не было ранее сделано вызовом Save()), но только обеспечив непросачивание исключений из этого вызова. Но лишь как last resort, не полагаясь на это при нормальном использовании класса.
Здравствуйте, smeeld, Вы писали:
S>Конструктор, предназначен для инициализации полей, деструктор, для освобождения ресурсов. S>Никаких тяжёлых операций, которые могут пройти неудачно, в них быть не должно. С++ не всесилен. S>При необходимости производить тяжёлые операции при инициализации и деинициализации, нужно S>делать всё по старинке как в Cи, разруливать в специализированных функциях: object.initialize(), object.deinitialize()
Очень жаль, а ведь счастье было так близко.) Про конструктор, кстати, не совсем понятно — почему не проводить там тяжелых операций если следить за тем, чтобы выполнение корректно завершилось при возникновении исключения посередине конструктора, basic exception safety вроде как для этого достаточно?
Если каждому объекту писать init/deinit кроме конструктора-деструктора, то все это не будет отличаться от С, по большому счету, мне кажется это очень сильно обесценивает все эти С++-навороты.( Лучше уж тогда ошибки игнорировать в деструкторе.(
Здравствуйте, jahr, Вы писали:
J>Про конструктор, кстати, не совсем понятно — почему не проводить там тяжелых операций если следить за тем, чтобы выполнение корректно завершилось при возникновении исключения посередине конструктора, basic exception safety вроде как для этого достаточно?
Если полноценная инициализация происходит только в случае успешного завершения тяжёлой операции,
например, открытие IO дескриптора, то неудача в этой операции обеспечит "неполноценность" объекта,
и после его создания эту полноценность всё равно придётся проверять чем-то вроде object.is_good().
Так можно вместо is_good() вызывать initialize() и смотреть возврат.
J>Если каждому объекту писать init/deinit кроме конструктора-деструктора, то все это не будет отличаться от С, по большому счету, мне кажется это очень сильно обесценивает все эти С++-навороты.( Лучше уж тогда ошибки игнорировать в деструкторе.(
С++ не всесилен, несмотря на наличие ++ во основном он недалеко от Си оторвался.
ЗЫ С++ навороты вовсе не в конструкторах/деструкторах.
Здравствуйте, smeeld, Вы писали:
S>ЗЫ С++ навороты вовсе не в конструкторах/деструкторах.
Многи люди (и я в том числе) считают, что деструкторы — одна из главных фич C++. Где-то (на StackOverflow?) был вопрос про самый красивый, мощный и выразительный однострочник на C++, и в качестве образца приводился вот этот код:
Здравствуйте, landerhigh, Вы писали:
L>Здравствуйте, jahr, Вы писали:
L>ИМХО ответ на этот вопрос лежит в самом определении деструктора. L>Деструктор вызывается при разрушении объекта. То есть, что бы не случилось в Вега деструкторе, остается в деструкторе. И вызываемый код doesn't care. L>Отсюда и плясать.
Как-то это неправильно получается — язык by-design подразумевает наличие каких-то "черных дыр" кода, это ж почти undefined behaviour.)
L>"Запись в файл" — это не "освобождение ресурса". Это и правда весьма сложное действие, которое может обломаться. В зависимости от стратегии обработки ошбиок, внешней логике на это может быть пофигу, тогда в принципе это можно и в деструкторе делать. L>А может и не пофигу, тогда подобному действию не место в деструкторе.
Ну, можно найти пример и не так явно сложный, например, — многие winapi-шные функции освобождения ресурсов имеют код возврата, при работе с этими ресурсами такие функции обычно помещают в деструктор, стандартно игнорируя этот код возврата, что периодически (хоть и редко) приводит к неочевидным ошибкам и сложной отладке.
Просто такая проблема стандартно возникает при работе со сценариями, похожими на описанный сценарий с файлом, я 10 лет назад спотыкался о них, выбирая писать ли С-подобный нечитаемый код или пожертвовать надежностью, и сейчас также задумываюсь об этом, не смотря на все бусты и С++14. Ведь решили же подобную застарелую проблему с ненужными копированиями при помощи movable-семантики, я подумал, что может я что-то пропустил и сейчас возможно красиво в плюсовом стиле прописать и эту стандартную задачу с чтением-записью в файл.)
Здравствуйте, Qbit86, Вы писали:
Q>Многи люди (и я в том числе) считают, что деструкторы — одна из главных фич C++.
Создание объекта с его автоматической инициализацией можно и в Cи реализовать макросами.
Для меня главной фичей С++ является STL, которые ускоряют и разработку, сам синтаксис С++
ничего кардинально лучшего по сравнению с Си не предлагает.
Здравствуйте, jahr, Вы писали:
S>>Конструктор, предназначен для инициализации полей, деструктор, для освобождения ресурсов. S>>Никаких тяжёлых операций, которые могут пройти неудачно, в них быть не должно. С++ не всесилен. S>>При необходимости производить тяжёлые операции при инициализации и деинициализации, нужно S>>делать всё по старинке как в Cи, разруливать в специализированных функциях: object.initialize(), object.deinitialize() J>Очень жаль, а ведь счастье было так близко.)
Тяжёлые операции в конструкторе — без проблем.
Тяжёлые операции в деструкторе — можно, но нужно следить за исключениями.
Здравствуйте, jahr, Вы писали:
L>>Деструктор вызывается при разрушении объекта. То есть, что бы не случилось в Вега деструкторе, остается в деструкторе. И вызываемый код doesn't care. L>>Отсюда и плясать.
J>Как-то это неправильно получается — язык by-design подразумевает наличие каких-то "черных дыр" кода, это ж почти undefined behaviour.)
Нет тут никакой черной дыры. Деструктор вызывается при разрушении объекта. Объект разрушается, когда вызываемый код решает "все, я с тобой закончил, ты мне больше не нужен". Это подразумевает, что все, что вызываемому коду нужно было от объекта, он уже получил.
А объект, в свою очередь, способен гарантированно подтереть за собой в деструкторе.
Это же очевидно.
L>>"Запись в файл" — это не "освобождение ресурса". Это и правда весьма сложное действие, которое может обломаться. В зависимости от стратегии обработки ошбиок, внешней логике на это может быть пофигу, тогда в принципе это можно и в деструкторе делать. L>>А может и не пофигу, тогда подобному действию не место в деструкторе.
J>Ну, можно найти пример и не так явно сложный, например, — многие winapi-шные функции освобождения ресурсов имеют код возврата,
Это зависит от нескольких вещей. Во-первых, важен ли нам код возврата. Во-вторых, можем ли мы что-то сделать в случае, если код ошибки возвращает ошибку. В-третьих, это скорее зависит от стратегии обработки ошибок в программе.
Все эти вопросы прямого отношения к деструкторам не имеют.
Здравствуйте, jahr, Вы писали:
> Ведь решили же подобную застарелую проблему с ненужными копированиями при помощи movable-семантики
Это было решение проблемы самого С++, когда объекты копировались, Вы же предлагаете
решение глобальных проблем, типа невозможности выделения ресурсов ядром ОС, разруливать из
пользовательской программы на C++ автоматически, это С++ должен будет контролировать ядро ОС не больше
и не меньше.
Здравствуйте, Evgeny.Panasyuk, Вы писали:
EP>Здравствуйте, jahr, Вы писали:
J>>В общем, буду признателен, если вы поделитесь своими способами реализовать описанный в примере сценарий работы идеологически правильным способом.)
Здравствуйте, landerhigh, Вы писали:
L>>>Деструктор вызывается при разрушении объекта. То есть, что бы не случилось в Вега деструкторе, остается в деструкторе. И вызываемый код doesn't care. L>>>Отсюда и плясать. J>>Как-то это неправильно получается — язык by-design подразумевает наличие каких-то "черных дыр" кода, это ж почти undefined behaviour.) L>Нет тут никакой черной дыры. Деструктор вызывается при разрушении объекта. Объект разрушается, когда вызываемый код решает "все, я с тобой закончил, ты мне больше не нужен". Это подразумевает, что все, что вызываемому коду нужно было от объекта, он уже получил. L>А объект, в свою очередь, способен гарантированно подтереть за собой в деструкторе. L>Это же очевидно.
Деструкторы могли бы нормально кидать исключения, например если бы многократные исключения аккумулировались в одно.
В случаях же где из-за алгоритмических особенностей нужен не кидающий деструткор — использовали бы noexcept.
Даже была статья на эту тему — "Destructors That Throw: Evil, or Just Misunderstood? — Jon Kalb and Dave Abrahams" (оригинальный сайт выпилен, а копию/кэш я сходу не нашёл). Там была фраза, что-то в духе: "делать std::terminate в таких случаях это слишком драконовские меры".
EP>Даже была статья на эту тему — "Destructors That Throw: Evil, or Just Misunderstood? — Jon Kalb and Dave Abrahams" (оригинальный сайт выпилен, а копию/кэш я сходу не нашёл). Там была фраза, что-то в духе: "делать std::terminate в таких случаях это слишком драконовские меры".
So there you have it. The reason we can’t have throwing destructors is that nobody worked out how to deal with multiple exceptions wanting to propagate through the same set of stack frames. Considering the fact that the program knows how to unwind from here, even if it doesn’t know exactly what to propagate, and the fact that it’s so easy to throw from a destructor by mistake, we think termination is a bit draconian.
Frankly, we don’t think it’s so hard to nail down the final details of how this should work. For example, it might be reasonable to simply drop the second exception on the floor and propagate the original one. Before you freak out, consider this: the second exception doesn’t change the unwinding process in any way, at least, not until the exception is caught, and the original failure is still the root cause of the current unwind. The program or the user can likely deal just as well with that root cause without knowing anything about the second exception.
This being C++, we expect someone to want more control over that second exception, so in our next installment, we’ll consider some alternatives. For now, we leave you with the suggestion that maybe destructors that throw are not truly Evil™, but just misunderstood.
Здравствуйте, smeeld, Вы писали:
>> Ведь решили же подобную застарелую проблему с ненужными копированиями при помощи movable-семантики
S>Это было решение проблемы самого С++, когда объекты копировались, Вы же предлагаете S>решение глобальных проблем, типа невозможности выделения ресурсов ядром ОС, разруливать из S>пользовательской программы на C++ автоматически, это С++ должен будет контролировать ядро ОС не больше S>и не меньше.
Нет, ядро здесь совсем не при чем. В С++ есть возможность автоматически выполнять какие-то операции (деструкторы), но на эти операции наложено очень сильное ограничения — они не могу возвращать значений и кидать исключения, что при аккуратном отношении к логике выполнения практически лишает программиста возможности использовать это автоматическое выполнение кода за исключением самых тривиальных случаев.
Здравствуйте, jahr, Вы писали:
L>>А объект, в свою очередь, способен гарантированно подтереть за собой в деструкторе. L>>Это же очевидно.
J>Вопрос как раз в том, что делать если подтереть не получилось.)
Застрелиться aka terminate(). Есть другие варианты?
Здравствуйте, Qbit86, Вы писали:
Q>Здравствуйте, jahr, Вы писали:
Q>Запись в файл делай в отдельном методе Save(), или Flush(), или Close(), etc. В коде явно вызывай этот метод (потенциально бросающий исключение) в нормальном потоке выполнения, не полагаясь на деструктор. Q>В деструкторе тоже можешь попытаться вызвать этот метод или его содержимое (проверив, что объект «грязный» и сохранение действительно нужно, и не было ранее сделано вызовом Save()), но только обеспечив непросачивание исключений из этого вызова. Но лишь как last resort, не полагаясь на это при нормальном использовании класса.
Тогда мне придется каждое использование этого класса обкладывать try'ем, в catch'е которого вызывать этот метод Save, использование класса станет мучением, эти перехваты везде писать будет лениво, я начну это пропускать, начнутся ошибки и понесется.) Тогда уж проще вообще отказаться от исключений и работать только на кодах возврата как в С.)