Обновить
86
1.3
Евгений Охотников@eao197

Велосипедостроитель, программист-камикадзе

Отправить сообщение

Рассуждения о плохости неудачности дизайна std::filesystem выглядят пустопорожними, потому что нет примеров того, как это должно было бы выглядеть по мнению автора статьи.

Чем меньше скоуп проблемы, тем проше с ней справляться при её возникновении.

Как раз с точки зрения простоты решение с конструктором проще, чем с функцией-фабрикой. Грубо говоря, с конструктором и исключениями у нас:

class my_class {
  resource_one m_one;
  resource_two m_two;

public:
  my_class(param_one one, param_two two)
  {
    // Допустим, у нас здесь может быть исключение.
    check_something(one, two);
    m_one = make_resource_one(one);

    // Допустим, здесь у нас может быть еще одно исключение.
    check_something_else(one, two, m_one);
    m_two = make_resource_two(two);
  }
};

И его аналог без бросающего конструктора:

class my_class {
  resource_one m_one;
  resource_two m_two;

private:
  my_class(resource_one one, resource_two two)
    // ПРИМЕЧАНИЕ: тут нам еще требуется, чтобы resource_one и
    // resource_two были moveable типами.
    : m_one(std::move(one))
    , m_two(std::move(two))
  {}

public:
  static my_class
  make(param_one one, param_two two)
  {
    // Допустим, у нас здесь может быть исключение.
    check_something(one, two);
    auto res_one = make_resource_one(one);

    // Допустим, здесь у нас может быть еще одно исключение.
    check_something_else(one, two, m_one);
    auto res_two = make_resource_two(two);

    return { std::move(res_one), std::move(res_two) };
  }
};

Как-то телодвижений сильно больше.

Когда требуется полностью небросающие конструкторы (оптимизация из-за того, что у нас объекты создаются на hot-path), тогда хотя бы понятно зачем это все. Но, имхо, в подавляющем большинстве случаев это все не нужно.

Чтобы семантически отделять инициализацию объекта от валидации.

Видимо, все дело в этом. Поскольку не ясно зачем это отделение требуется.

Понятно, что вы рассказываете свою точку зрения. И у меня нет цели вас переубедить. Просто хочется понять чем руководствуются люди, критирующие исключения (или отдельные моменты, связанные с исключениями). Но логики в ваших рассуждения, увы, не нахожу.

На самом деле вопросы и тезисы у @pavlushk0 более чем правильные.

Почему-то (непонятно почему) исключения из конструктора -- это фу-фу-фу, а вот исключения из функции-фабрики, которую придется дергать вместо конструктора, это уже нормально.

Это не проблема исключений. Если вы компилируетесь без явного запрета исключений на уровне компилятора, то вам в любом случае нужно быть готовым к тому, что исключение может вылететь.

В этом контексте геморроя добавляют noexcept-контексты, о которых я говорил в другом комментарии. Вот вылет исключения из noexcept-метода или деструктора -- вот это реальная проблема, которая требует внимательности.

Программа в таком случае запросто может завершить работу.

И это многократно лучше, чем когда программа продолжает работать с мусорными данными внутри и все равно падает, но с другой диагностикой и в другом месте, да еще и что-нибудь запортив по дороге (например, сохранив какой-то мусор на диске вместе актуальных данных пользователя).

Невозможно быть опытным во всех возможных областях.

Здесь, к счастью, всего одна область -- это знание С++. Точне даже не "знание", а понимание того, как минимизировать шансы отстрелить себе ногу.

А у Вас в каких случаях исключения активно используются?

Еще пример. Доводилось делать специализированые "умные указатели" (что-то вроде прокси-указателей, а не аналогов unique_ptr и shared_ptr). И там могло случиться так, что прокси-указатель становился пустым, т.е. начинал содержать значение nullptr. Такие ситуации контролировались в реализации методов operator* и operator->. Что-то вроде:

template<typename T>
class tricky_proxy_ptr {
  T * m_object;
public:
  ...
  [[nodiscard]] T&
  operator*() {
    if(!m_object) throw std::runtime_error{ "attempt to access empty proxy" };
    return *m_object;
  }
  [[nodiscard]] T*
  operator->() {
    if(!m_object) throw std::runtime_error{ "attempt to access empty proxy" };
    return m_object;
  }
  ...
};

Если эти if-ы начинают сказываться на производительности, то для RELEASE-сборок их приходится вырезать через #if/#endif

Не очень понимаю, что такое "вычислительный код"

Грубо говоря, когда код реализует какую-то математику. Типа преобразования аудио из 96 кГц и 32-bit floating point в 44.1 кГц и 16-бит integer (условно, пример от балды).

А у Вас в каких случаях исключения активно используются?

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

Исключения используются для информирования о ситуациях, которые не должны были бы возникнуть, но возникли. Например, пусть будет условный класс config_file, конструктор которого получает std::filesystem::path:

class config_file {
  ...
public:
  explicit config_file(
    // Не должен быть пустым.
    const std::filesystem::path & path);
  ...
};

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

Ну так и с исключениями можно наступить на грабли.

Худшее, что у вас может возникнуть с исключениями -- это то, что вы его не поймаете. И это гораздо, гораздо лучше, чем когда вы продолжите исполнение с неверными данными.

Это тоже вполне себе бомба замедленного действия.

Это не бомба.

нормальные авторы обычно в документации пишут, кидают ли они исключения.

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

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

На самом неделе не суть важно, используются ли для информирования об ошибках исключения или коды возврата. Если взять себе за правило обеспечивать хотя бы basic exception safety (т.е. отсутствие утечек ресурсов при преждевременном выходе), то становится без разницы используются ли исключения или нет. И ключевым методом здесь как раз становится тот самый RAII, речь о котором и идет в этой посредственного качества статье.

Возможно, для кого-то это усложнение, не спорю.

Это не усложнее. Больше похоже на обман самого себя.

Впрочем, если вы пишете в основном вычислительный код, то тема исключений для вас, вероятно, не особо актуальна.

Интересно, и как вы решаете проблему нарушения инвариантов в конструкторе? Т.е. конструируется объект, в процессе выясняется, что создать в нормальном состоянии его нельзя и создание нужно прервать.

Может ваши разработки есть в OpenSource? Вы тогда бы просто дали ссылку на соответствующий фрагмент, это сэкономило бы время.

Я уже написал, что сам привык работать без обработки исключений. И мне не нравится, когда вызываешь чужой код, а он роняет мою программу в непредсказуемый момент при обработке каких-то входных данных (и не всегда очевидно, каких именно).

Правильно ли я понимаю, что вы запрещаете исключения на уровне ключей компилятора?

Можно ни о чем не думать.

А если не сваливаться в морализаторство и попробовать обсудить чем программирование в присутствии исключений отличается от программирования в отсутствии оных?

Двайте сейчас на брать в рассмотрение специальных noexcept-контекстов (вроде кода деструкторов, секций catch и noexcept-методов). Просто обычный код.

Что такое привносят исключения, что требует переосмысления при написании кода?

все время помнить, что в любой строке может быть выкинуто исключение, которое может быть отловлено вообще не здесь, а в вызывающей процедуре

А зачем об этом помнить и думать?

Стиль написания кода при использовании исключений все же серьезно отличается,

Чем же и почему?

и это влияет и на читабельность кода.

И что, на ваш взгляд, читается лучше -- с исключениями или без?

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

А апелляции к тому, что так не было принято в Facebook-е или Google без уточнения конкретных причин -- ну это просто пиетет перед громкими именами и ничего более.

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

ЕМНИП, правило не бросать исключения из конструктора в свое время насаждалось не потому, что были некие непривыкшие ловить исключения программисты. А потому, что еще до появления широкой поддержки исключений в большинстве C++ных компиляторов (а это произошло, увы далеко не сразу) появилось множество библиотек, которые не были расчитаны на применение исключений вообще. Навскидку вспоминаются Qt и MFC, которые использовались ну очень широко.

И вот в таких библиотеках применять исключения было чревато. В том числе и в конструкторах.

Т.е. основная причина не в том, что выброс исключения из конструктора для кого-то из программистов может быть неожиданностью. А в том, что применение исключений (в том числе и из конструкторов) было чревато при интеграции со старыми библиотеками, которые создавались еще без оглядки на наличие исключений в языке.

AFAIK, запрет на исключения в печально знаменитом Google Code Guide имеет те же самые корни.

Скорее к исключениям в принципе как источнику неявно отстрелить себе ногу.

Ну т.е. дело вовсе не в конструкторах, а в исключениях.

оно может быть и вовсе без опционалов. что-нибудь типа

Все эти кружева пайплайнов базируются на том, что в случае чего исключение прервет и конструирование пайплайна, и работу пайплайна.

Вы спросили мои предложения - я вам предоставил.

Справедливости ради, не я спрашивал у вас предложения.

Что бы лично я спросил, так это:

А уж как замечательно кидать исключения в конструкторе

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

Ну и раз заговорили про инварианты - мне не нравится логика, когда мы исходя из сломанных объектов пытаемся собрать валидный объект.

А откуда эта попытка возьмется, если конструкторы будут бросать исключения при невозможности корректной инициализации объекта?

Ну а для hot path так и вовсе сначала б данные подготавливал, а не исключения кидал, извращая поток данных.

Собственно я об этом и говорил перечисляя случаи когда порождающая функция-фабрика с std::expected оправдана. Не вижу противоречия.

Одно другому вряд ли помешает.

1
23 ...

Информация

В рейтинге
1 635-й
Откуда
Гомель, Гомельская обл., Беларусь
Зарегистрирован
Активность