Является ли std :: bad_optional_access небольшим преступлением против исключений? - PullRequest
0 голосов
/ 27 марта 2020

Если функция std::optional 'value() вызывается, когда optional не имеет инициализированного фактического значения, выдается std::bad_optional_access. Поскольку он получен непосредственно из std::exception, вам нужно либо catch (std::bad_optional_access const&), либо catch (std::exception const&) для работы с исключением. Однако оба варианта кажутся мне грустными:

  • std::exception перехватывает каждое исключение
  • std::bad_optional_access раскрывает детали реализации. Рассмотрим следующий пример:
Placement Item::get_placement() const {
  // throws if the item cannot be equipped
  return this->placement_optional.value();
}
void Unit::equip_item(Item acquisition) {
  // lets the exception go further if it occurs
  this->body[acquisition.get_placement()] = acquisition;
}
// somewhere far away:
try {
  unit.equip_item(item);
} catch (std::bad_optional_access const& exception) { // what is this??
  inform_client(exception.what());
}

Итак, чтобы поймать исключение, вы должны быть хорошо осведомлены об использовании std::optional в реализации Item, будучи привел к списку уже известных проблем. Также я не хочу поймать и перефразировать std::bad_optional_access, потому что (для меня) ключевой частью исключений является возможность игнорировать их до тех пор, пока они не понадобятся. Вот как я вижу правильный подход:

std::exception
  <- std::logic_error
    <- std::wrong_state (doesn't really exist)
      <- std::bad_optional_access (isn't really here)

Итак, пример "далеко" можно написать так:

try {
  unit.equip_item(item);
} catch (std::wrong_state const& exception) { // no implementation details
  inform_client(exception.what());
}

Наконец,

  • Почему std::bad_optional_access разработан так, как он?
  • Правильно ли я чувствую исключения? Я имею в виду, были ли они введены для такого использования?

Примечание: boost::bad_optional_access происходит от std::logic_error. Отлично!

Примечание 2: Я знаю о catch (...) и метаниях предметов, отличных от std::exception семейства. Они были опущены для краткости (и здравомыслия).

Обновление: к сожалению, я не могу принять два ответа, поэтому: если вас интересует топи c, вы можете прочитать ответ Зуодиана Ху и их Комментарии.

Ответы [ 5 ]

3 голосов
/ 27 марта 2020

Вы говорите, что ключевая привлекательность исключений заключается в том, что вы можете игнорировать их настолько глубоко, насколько можете. Предположительно, учитывая ваше стремление избежать утечки деталей реализации, вы больше не можете позволить исключению распространяться за пределы точки, когда это исключение не может быть понято и исправлено его обработчиком. Похоже, это противоречит вашему идеальному дизайну: он пытается исправить исключение для пользователя, но bad_optional_access::what не имеет точного контекста о том, что только что произошло - утечка деталей реализации пользователю. Как вы ожидаете, что пользователь предпримет осмысленное действие против отказа в оснащении предмета, когда все, что он видит, в лучшем случае «не может экипировать предмет: bad_optional_access»?

Вы, очевидно, сделали чрезмерное упрощение, но проблема остается. Даже с «лучшей» иерархией исключений, std::bad_optional_access просто не имеет достаточного контекста, чтобы что-то, кроме чрезвычайно близких абонентов, могло знать, что с ним делать.

Есть несколько довольно разных случаев, в которых вы можете захотеть throw:

  1. Вы хотите, чтобы поток управления был прерван без особых синтаксических издержек. Например, у вас есть 25 различных опций, которые вы хотите развернуть, и вы хотите вернуть специальное значение, если какой-либо из них потерпит неудачу. Вы устанавливаете блок try-catch вокруг 25 доступов, сохраняя себя 25 if блоков.
  2. Вы написали библиотеку для общего пользования, которая делает много вещей, которые могут go ошибаться, и вы хотите сообщать мелкозернистым ошибкам вызывающей программе, чтобы дать ей наилучшую возможность программно сделать что-то умное для восстановления.
  3. Вы написали большую платформу, которая выполняет задачи очень высокого уровня, так что вы ожидаете, что обычно единственный разумный результат неудачной операции - это сообщить пользователю, что операция не удалась.

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

Очевидно, комитет C ++ считает, что bad_optional_access принадлежит к первой категории, и вы спрашиваете, почему это не часть третьей категории. Вместо того, чтобы пытаться игнорировать исключения до тех пор, пока вам «не нужно» что-то с ними сделать, я считаю, что вы должны перевернуть вопрос и спросить себя, что намеревается поймать исключение.

Если ответ действительно таков, user ", тогда вы должны выбросить что-то, что не является bad_optional_access и вместо этого имеет высокоуровневые функции, такие как локализованные сообщения об ошибках и достаточно данных, чтобы inform_user смог вызвать диалог со значимым заголовком, основным текстом, подтекст, кнопки, значок и т. д. c.

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

Если ответ таков, что вы можете попытаться оборудовать 25 предметов подряд и хотите остановиться, как только что-то пойдет не так, вам не нужно вносить изменения в bad_optional_access.

Также обратите внимание, что различные реализации делают Различное использование более или менее удобно. В большинстве современных реализаций C ++ нет никаких накладных расходов на пути кода, которые не генерируют, и огромных накладных расходов 1033 * на пути, которые бросают. Это часто подталкивает к использованию исключений для первой категории ошибок.

3 голосов
/ 27 марта 2020

Итак, чтобы поймать исключение, вы должны быть хорошо проинформированы об использовании std::optional в реализации элемента

Нет, чтобы поймать исключение, вы должны прочитать документация для get_placement, которая скажет вам, что она выбрасывает std::bad_optional_access. Выбрав отправку этого исключения, функция делает передачу этого исключения частью интерфейса этой функции.

И, следовательно, она больше не зависит от Item ' Реализация, чем если бы он прямо вернул std::optional. Вы решаете поместить его в свой интерфейс, так что вам следует смириться с последствиями.

Другими словами, если вы чувствовали, что указание std::optional в качестве типа параметра или возвращаемого значения было неправильным, то вы должно быть то же самое по отношению к прямому испусканию bad_optional_exception.


В конечном счете, все это восходит к одному из самых фундаментальных вопросов обработки ошибок: как далеко вы можете добраться до места возникновения ошибки прежде чем специфицированный c характер ошибки станет фактически бессмысленным или даже совершенно другим?

Допустим, вы выполняете обработку текста. У вас есть файл с каждой строкой, содержащей 3 числа с плавающей точкой. Вы обрабатываете это построчно и вставляете каждый набор из трех значений в список. И у вас есть функция, которая преобразует строки в числа с плавающей точкой, которая выдает исключение, если это преобразование не удается.

Итак, код в целом выглядит следующим образом:

for each line
  split the line into a 3-element list of number strings.
  for each number string
    convert the string into a number.
    add the number to the current element.
  push the element into the list.

Хорошо, так что ... что произойдет, если ваш конвертер строки в плавающий выбрасывает? Это зависит от; что вы хотите, чтобы произошло? Это определяется тем, кто ловит это. Если вам нужно значение по умолчанию для ошибки, тогда код в самом внутреннем l oop перехватывает его и записывает значение по умолчанию в элемент.

Но, возможно, вы хотите записать, что в конкретной строке есть ошибка , затем пропустите эту строку (не добавляйте ее в список), но продолжайте обрабатывать остальную часть текста как обычно. В этом случае вы ловите исключение в первом l oop.

В этот момент значение ошибки изменилось . Было выдано сообщение об ошибке: «эта строка не содержит допустимого числа с плавающей запятой», но ваш код не справляется с этим. Фактически, код перехвата полностью потерял контекст ошибки. Он не знает, была ли первая, вторая или третья строка в тексте причиной ошибки. В лучшем случае, он знает, что это было где-то вдоль этой линии, и, возможно, исключение содержит пару указателей на диапазон плохих строк (хотя это становится все более опасным, чем дальше это исключение получает из своего источника, из-за возможности висячих указателей ).

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

Чем дальше вы получаете от того, где выдается исключение, тем больше теряется контекст об ошибке, и больше значение в конечном итоге отклоняется от первоначального значения ошибки. Это не просто деталь реализации; речь идет о местонахождении информации и ответе на эту информацию.

Таким образом, в основном код, близкий к источнику ошибки, перехватывает определенные c исключения с контекстным значением. Чем дальше перехватчик получает от источника ошибки, тем более вероятно, что код перехвата будет очень обобщенным c, имея дело с расплывчатым «это не работает по причинам». Вот тут-то и появляются неопределенные типы, такие как std::logic_error.

Действительно, можно представить, что на каждом этапе процесса исключение интерпретируется по-новому (и под «переосмыслением» я имею в виду преобразование его в другой тип). через catch/throw). Конвертер string-to-float выдает значимое исключение: не удалось преобразовать строку в float. Слой, пытающийся построить элемент из 3 строк, преобразует исключение во что-то, что имеет значение для его вызывающего: строковый индекс X искажен. И на последнем этапе исключение обобщается на: не удалось проанализировать список из-за строки Y.

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

Наследование от logic_error неверно по этим причинам. Поймать bad_optional_access в конечном итоге очень локально. После определенного момента значение этой ошибки меняется.

«logi c error» означает, что ваша программа не имеет смысла. Но необязательный параметр, который не содержит значения, не обязательно означает, что . В одном фрагменте кода может быть совершенно правильным иметь пустой необязательный аргумент, и выбрасываемое исключение - это просто то, как об этом сообщается вызывающей стороне. В другом фрагменте кода необязательный элемент может быть пустым в определенный момент как пользователь, совершивший некоторую предыдущую ошибку при использовании API. Одной из них является ошибка logi c, а другой - нет.

В конечном счете, правильное, что нужно сделать, это убедиться, что все ваши API классов генерируют исключения, которые значимы для звонящего. И не ясно, что bad_optional_access означает для абонента get_placement.

2 голосов
/ 27 марта 2020

Предоставление сведений о реализации

Если вы sh не захотите, чтобы ваш пользователь не знал о std::optional в вашей реализации, ваш интерфейс либо проверит operator bool или has_value и выполнит одно из следующих действий: следующее:

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

... или ваш интерфейс перехватит std::bad_optional_access и выполнит одно из перечисленных действий. В любом случае ваш клиент не знает, что вы использовали std::optional.

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

Logi c Ошибка?

На основе концептуальной модели для факультативного в документе перед стандартизацией , std::optional - это оболочка значений с valid пустое состояние. Следовательно, целью было пустота быть преднамеренной при нормальном использовании. Как я указал в комментариях, есть два основных способа обращения с пустотой:

  1. используйте operator bool или has_value, затем обрабатывайте пустую строку или используйте упакованное значение через operator* или operator-> .
  2. используйте value и выйдите из области видимости, если опционально пусто

В любом случае вы должны ожидать, что optional потенциально будет пустым и разработанным для того, чтобы это было допустимым состоянием в вашей программе.

Другими словами, когда вы используете operator bool или has_value для проверки на пустоту, это не предотвращает возникновение исключения. Вместо этого вы решаете вообще не использовать интерфейс исключений optional (обычно). И когда вы используете value, вы выбираете принять optional потенциально бросая std::bad_optional_access. Следовательно, исключение никогда не должно быть ошибкой logi c при предполагаемом использовании optional.

UPDATE

Logi c Ошибки при разработке C ++

Похоже, вы неправильно поняли определение стандарта, что такое ошибка logi c.

В последние годы в дизайне C ++ (не то же самое в истории) ошибка logi c является ошибкой программиста, из-за которой приложение не должно пытаться восстанавливаться, поскольку оно не может быть восстановлено разумным образом. Это включает в себя такие вещи, как разыменование висячих указателей и ссылок, использование operator* или operator-> в пустом необязательном аргументе, передача недопустимых аргументов в функцию или иное нарушение контрактов API. Обратите внимание, что существование висячих указателей не является ошибкой logi c, но разыменование висячего указателя является ошибкой logi c.

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

Когда хорошо спроектированная (в соответствии с этой философией) функция стандартной библиотеки выдает, это никогда не должно происходить, потому что код или звонивший написал глючный код. Что касается глючного кода, Стандарт позволяет вам не обращать внимания на написание ошибки. Например, многие функции в <algorithn> запускают бесконечные циклы, если вы передаете им плохие итераторы begin и end, и никогда даже не пытаетесь диагностировать тот факт, что вы это сделали. Они, конечно, не бросают std::invalid_argument. «Хорошие» реализации все же пытаются диагностировать это в сборках Debug, потому что эти ошибки logi c являются bugs . Когда сработает хорошо спроектированная (в соответствии с этой философией) функция стандартной библиотеки, это происходит потому, что произошло действительно исключительное и неизбежное событие. имеет много функций выброса, потому что вы не можете точно знать, что находится в какой-то случайной файловой системе. В этой ситуации исключения должны использоваться.

В статье, приведенной ниже, Херб Саттер выступает против существования std::logic_error как исключительного типа по этой самой причине. Четко сформулировав философию, перехват std::logic_error или любого из его дочерних элементов равносилен введению накладных расходов времени выполнения для исправления ошибок программиста c. Любое истинное состояние ошибки logi c, которое вы хотите обнаружить, действительно должно быть подтверждено, чтобы об ошибке можно было сообщать людям, написавшим ошибку.

В интерфейсе optional, разработанном с учетом вышесказанного, value выбрасывает, так что вы можете программно с ним справиться разумным образом, ожидая, что тот, кто его поймает, либо не заботится о том, что bad_optional_access означает (catch( ... ) // catch everything) или может конкретно иметь дело с bad_optional_access. Это исключение не должно распространяться далеко вообще. Когда вы намеренно вызываете value, вы делаете это, потому что признаете, что optional может быть пустым, и вы решаете выйти из текущей области, если она окажется пустой.

См. Первый раздел этой статьи (скачать) для философского обоснования.

1 голос
/ 27 марта 2020

Во-первых, если вы не хотите раскрывать реализацию, то исключения не должны даже пересекать границу между реализацией и клиентским кодом. Это распространенная идиома, что ни одно исключение не должно пересекать границы библиотек, API и т. Д. c.

Далее, тот факт, что вы храните что-то в optional, является реализацией, которую вы должны контролировать самостоятельно. , Это означает, что вы должны проверить, что опциональное не пусто (по крайней мере, если вы не хотите, чтобы клиент знал детали реализации).

И, наконец, ответьте на вопрос: является ли ошибкой то, что клиентский код выполняет операцию над пустым объектом? Если это то, что разрешено делать , то исключение не должно создаваться (например, может быть возвращен код ошибки). Если это реальная проблема, которая не должна возникать, целесообразно создать исключение. Вы можете поймать std::bad_optional_access в своем коде и выбросить что-то еще из блока catch.

0 голосов
/ 01 апреля 2020

Другим решением вашей проблемы могут быть вложенные исключения . Это означает, что вы перехватываете исключение более низкого уровня (в вашем случае std::bad_optional_access), а затем выбрасываете другое исключение (любого типа, в вашем случае вы можете реализовать wrong_state : public std::logic_error) с помощью функции std::throw_with_nested. Используя этот подход, вы:

  1. Сохраните информацию об исключении нижнего уровня (оно все еще сохраняется как вложенное исключение)

  2. Скрыть эту информацию о вложенном исключении от пользователя

  3. Разрешить пользователю перехватывать исключение как wrong_state или std::logic_error.

см. пример: https://coliru.stacked-crooked.com/view?id=b9bc940f2cc6d8a3

...