Pull to refresh

Comments 16

Всегда не хватало в C++ возможности вызывать перед деструкторм просто виртуальный метод
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. Как по мне, так двухфазная инициализация — усложняет код и лучше ее избегать. То что я видел:


  1. Использование init() чтобы обработать ошибки создания объекта в случае когда эксепшины отключены.
    Иметь is_valid() — это хорошая альтернатива.
  2. Использование 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. Даже с примером выше — все красиво можно передать в качестве аргументов конструктора базового класса, если ему нужны какие-то дополнительные данные.
Я могу ошибаться, но не представляю случая где двухэтапной инициализации нельзя избежать.
Может у вас есть конкретный пример ?

1. Если делать клас TThread то join нельзя совать в деструктор, а в done можно.
2. Если у вас если загрузка контролов. То её лучше разместить в init но не в конструкторе.
и т.п.

Как по мне, всё равно — сомнительно: в случае активного эксепшина, вы все равно пишите в лог, то есть оригинальный эксепшн — теряется. Если на нем построена какая-то логика приложения (восстановки, повторов, прочее) — это логика также не будет выполнена. Если это нормально — то есть не делать какую-то обработку такого-то эксепшина — то почему-бы его и не выбрасывать совсем? Мне кажется, что у вас это имеет смысл из-за второй части статьи: все эксепшины — собираются вместе, ничего не игнорируется и потом все обрабатывается. Но, опять-же, при таком подходе — все деструкторы noexcept, то есть все что сказано в первой части статьи — не имеет смысла.


По поводу:


Когда очень-очень хочется выбросить сразу несколько исключений

мне кажется или вы говорите об std::throw_with_nested и семействе [1] ?


[1] nested_exception: https://en.cppreference.com/w/cpp/error/nested_exception

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

Что касается std::nested_exception — я рассматривал и их, но там всё же несколько про другое. Я думал некоторое время над тем, как решить проблему обработки множественных исключений, прилетающих из библиотечного кода, с помощью std::nested_exception, но придумать не смог. Этот механизм требует всё же изначально выбрасывать исключения не совсем стандартным способом. Если вам известно, как можно приспособить этот механизм для обсуждаемых в статье примеров, мне будет очень интересно его увидеть.
UFO just landed and posted this here
А можно мне для расширения кругозора рассказать, в каких случаях (кроме невалидного указателя) закрытие файла может завершиться ошибкой?
деструктор почти любого файлового сокета перед закрытием вызывает flush, а он может провалиться если, например, кончилось место
И что Вы сможете сделать в обработчике этой ошибки?
попробовать сбросить не flush-нутый буфер и закрыть файл, например.
Могу ошибаться, но что-то меня терзают смутные сомнения, что это не сработает.
Доступность места должна проверяться при вызове write, несмотря на то, что данные скорее всего останутся в буфере. Хотя, если почитать вот это, то можно узнать, что это не всегда так. Но что вы будете в этом случае делать? Возможно, вы уже десятки раз записывали в файл, просто так исправить это не получится.
Ещё там пишут, что не проверять результат close — это ошибка. Но варианты, когда вызов будет не успешен:
  • Невалидный дескриптор — это логическая ошибка при программировании, во время исполнения её уже не исправить, можно лишь залогировать
  • Ошибка ввода-вывода — опять же, а что вы будете делать?
  • По поводу EINTR интересно почитать это — можно свести к рекомендации использовать fsync, а не надеяться на обработку ошибки close

Лично моё мнение: если в API функции «очистки» могут возвращать ошибку, то с API что-то не так.
Заголовок спойлера
Проигнорировать ошибку. Плохо, потому что мы скрываем проблему, которая может повлиять на другие части системы.
Написать в лог. Лучше, чем просто проигнорировать, но всё равно плохо, т.к. наша библиотека ничего не знает о политиках логирования, принятых в системе, которая её использует. Стандартный лог может быть перенаправлен в /dev/null, в результате чего, опять же, ошибку мы не увидим.
Вынести освобождение ресурса в отдельную функцию, которая возвращает значение или бросает исключение, и заставлять пользователя класса вызывать её самостоятельно. Плохо, потому что пользователь вообще может забыть это сделать, и мы получим утечку ресурса.
Выбросить исключение. Хорошо в обычных случаях, т.к. пользователь класса может поймать исключение и стандартным образом получить информацию о возникшей ошибке. Плохо во время раскрутки стека, т.к. приводит к std::terminate().


Можно еще пункт добавить
  • Вызвать в случае ошибки callback или подать сигнал(QT)

Но только очень осторожно.
Sign up to leave a comment.

Articles