Comments 16
В дополнение к сказанному в статье: интересный опыт перехода на новый стандарт (именно исключения из деструкторов) с использованием статического анализа (clang-tidy).
struct A {
A();
virtual void init();
virtual void done();
virtual ~A();
};
struct B : A {
virtual void init();
virtual void done();
};
Так что бы после new B() сразу вызывался init(), а перед delete — done(); Приходилось это делать с помощью смартпоинтеров.
В том контексте что привели вы, init()
после new B()
приведет к вызову все того-же B::init()
, то есть всю инициализацию можно сделать в конструкторе B
. Как по мне, так двухфазная инициализация — усложняет код и лучше ее избегать. То что я видел:
- Использование
init()
чтобы обработать ошибки создания объекта в случае когда эксепшины отключены.
Иметьis_valid()
— это хорошая альтернатива. - Использование
init()
для настройки "зависимостей" объекта в том случае когда существуют циклические и другие проблемные зависимости объектов.
Тут, к сожалению, нужно править код, если можно.
struct A {
A();
virtual void init() { fn(); }
virtual void fn();
virtual void done() { fn(); }
virtual void ~A();
};
struct B:A {
virtual void fn();
};
struct C:B {
virtual void fn();
};
// C* c=new C(); c->init(); ... c->done(); delete c;
A::init() вполне может оперировать элементами C. В то время как в конструкторе такой возможности нет. В деструкторе таже проблема. Т.к. они первым делом таблицу виртуальных функций заменяют на свою.
Если подумать, я представляю только один случай когда это действительно нужно: когда инициализацию подобьектов нужно выполнить "всю за один раз". То есть по сути, конструкторы базовых классов пустые, а инициализация всего происходит в init()
. Но, опять же, мне кажется это больше проблема дизайна класса и/или API. Даже с примером выше — все красиво можно передать в качестве аргументов конструктора базового класса, если ему нужны какие-то дополнительные данные.
Я могу ошибаться, но не представляю случая где двухэтапной инициализации нельзя избежать.
Может у вас есть конкретный пример ?
Как по мне, всё равно — сомнительно: в случае активного эксепшина, вы все равно пишите в лог, то есть оригинальный эксепшн — теряется. Если на нем построена какая-то логика приложения (восстановки, повторов, прочее) — это логика также не будет выполнена. Если это нормально — то есть не делать какую-то обработку такого-то эксепшина — то почему-бы его и не выбрасывать совсем? Мне кажется, что у вас это имеет смысл из-за второй части статьи: все эксепшины — собираются вместе, ничего не игнорируется и потом все обрабатывается. Но, опять-же, при таком подходе — все деструкторы noexcept
, то есть все что сказано в первой части статьи — не имеет смысла.
По поводу:
Когда очень-очень хочется выбросить сразу несколько исключений
мне кажется или вы говорите об std::throw_with_nested
и семействе [1] ?
[1] nested_exception: https://en.cppreference.com/w/cpp/error/nested_exception
Что касается std::nested_exception — я рассматривал и их, но там всё же несколько про другое. Я думал некоторое время над тем, как решить проблему обработки множественных исключений, прилетающих из библиотечного кода, с помощью std::nested_exception, но придумать не смог. Этот механизм требует всё же изначально выбрасывать исключения не совсем стандартным способом. Если вам известно, как можно приспособить этот механизм для обсуждаемых в статье примеров, мне будет очень интересно его увидеть.
Ещё там пишут, что не проверять результат close — это ошибка. Но варианты, когда вызов будет не успешен:
- Невалидный дескриптор — это логическая ошибка при программировании, во время исполнения её уже не исправить, можно лишь залогировать
- Ошибка ввода-вывода — опять же, а что вы будете делать?
- По поводу EINTR интересно почитать это — можно свести к рекомендации использовать fsync, а не надеяться на обработку ошибки close
Лично моё мнение: если в API функции «очистки» могут возвращать ошибку, то с API что-то не так.
Проигнорировать ошибку. Плохо, потому что мы скрываем проблему, которая может повлиять на другие части системы.
Написать в лог. Лучше, чем просто проигнорировать, но всё равно плохо, т.к. наша библиотека ничего не знает о политиках логирования, принятых в системе, которая её использует. Стандартный лог может быть перенаправлен в /dev/null, в результате чего, опять же, ошибку мы не увидим.
Вынести освобождение ресурса в отдельную функцию, которая возвращает значение или бросает исключение, и заставлять пользователя класса вызывать её самостоятельно. Плохо, потому что пользователь вообще может забыть это сделать, и мы получим утечку ресурса.
Выбросить исключение. Хорошо в обычных случаях, т.к. пользователь класса может поймать исключение и стандартным образом получить информацию о возникшей ошибке. Плохо во время раскрутки стека, т.к. приводит к std::terminate().
Можно еще пункт добавить
- Вызвать в случае ошибки callback или подать сигнал(QT)
Но только очень осторожно.
Разрушительные исключения