По-моему, вопрос изначально нужно поставить так: нужна ли раскрутка стека для обработки исключительной ситуации? Если мы предполагаем, что в ряде случаев она нужна (полезна), то следует предоставить функцию, генерирующую исключение. Если мы предполагаем, что в ряде случаев исключительную ситуацию нужно обрабатывать на месте и при этом использование кодов исключительных ситуаций возможно и получается заметно удобнее/эффективнее, чем использование try/catch, то следует предоставить функцию, сообщающую код исключительной ситуации. Соответственно, если подходят оба случая, то следует предоставить оба варианта (выше было показано, как это можно сделать). Отмечу, что это применимо к исключительным ситуациям вообще, а не только к ошибкам. Например, то, что пользователь захотел отменить какое-то длительное вычисление, ошибкой не является, но является исключительной ситуацией, и раскрутку стека при этом удобно выполнить через генерацию исключения (как видно, именно удобство механизма исключений даёт повод к его применению).
Re[3]: api design: return code or exception - formal criteri
Здравствуйте, Andrew S, Вы писали:
J>>О, я придумал формальный критерий: всегда должны использоваться исключения, потому что они обеспечивают гарантию непродолжения в случае ошибки, не давая програмисту неявно ошибиться.
AS>Удобство использования страдает. Пример — тот же File.
Да ладно, где оно там так уж фатально страдает?
В примере с перебором файла разницы практически никакой.
AS>В результате разработчик будет делать как ему удобнее — не проверять коды. Плюсом придется писать кучу апгрейдов из одного интерфейса в другой...
Имхо, удобнее как раз с исключениями — ничего явно не надо проверять и бояться, что что-то забыл проверить и пытаешься записать в неоткрытый файл (например).
Еще раз, идея следующая — коды ошибок должны рассмативаться как оптимизация (да, и еще один случай забыл — когда пишем небросающий код для обеспечения безопасности исключений).
Здравствуйте, minorlogic, Вы писали:
M>1. в конструкторе обертки над мютексом, мы не смогли создать именованный мютекс. В этом случае лучше кинуть ексепшен, потому что объект повиснет в невалидном состоянии. Если этого не делать , то пользователь класса может и забыть проверить валидное создание объекта.
Согласен.
И вообще, временный объект любого типа может быть участником выражения, а не отдельного стейтмента.
Прервать вычисление выражения можно только через исключение.
Либо срезать углы, тщательно проверяя операнды (подобно тому, как в плавающей арифметике поступают с NaN).
M>2. Объект поток не смог открыться и вернул напримр false в методе stream.open(), после этого любая попытка прочесть из него или записать должны кидать эксепшены.
А вот здесь можно и ассерты, ибо если пользователь не проверил возвращаемое значение — он ССЗБ.
Вообще, у потоков есть флаг ошибки, срезающий углы.
... << RSDN@Home 1.2.0 alpha 4 rev. 1237>>
Перекуём баги на фичи!
Re: api design: return code or exception - formal criteria
Здравствуйте, Andrew S, Вы писали:
AS>Если есть ссылки критерии, best practice или что-нибудь подобное — будет супер.
Скотт Майерс, "Эффективное использование C++: 35 новых рекомендаций", глава 3, в особенности Правило 15 "Оценивайте затраты на обработку исключений"
Почитал-почитал, и понял, почему разработчики известных мне библиотек так осторожно относятся к использованию исключений.
P.S. Хотя, возможно, эта книга устарела? Написана 13 лет назад, а, как мне снисходительно пояснил на собеседовании один "молодой, да ранний" гуру вкуривший непосредственно стандарт, "С тех пор C++ радикально изменился".
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[3]: api design: return code or exception - formal criteri
M>>2. Объект поток не смог открыться и вернул напримр false в методе stream.open(), после этого любая попытка прочесть из него или записать должны кидать эксепшены.
К>А вот здесь можно и ассерты, ибо если пользователь не проверил возвращаемое значение — он ССЗБ.
На мой взгляд в этом случае именно что ассерты. Пользователь продолжает пользовать невалидный объект — значит ошибка в логике программы, а ошибку в логике надо по возможности отлавливать никак не в релизе. Эксепшены только в случае, если слой более низкого уровня вернул "плохую" ошибку. На плохой хендл, например, многие api так и сделают, так что автоматически без "лишней" рантайм проверки в релизе все будет выглядеть как надо.
Но, тут появляется неожиданная тонкость. Возьмем, к примеру, unique_lock а-ля буст:
Правомерно ли тут использование именно исключения, а не ассерта? На самом деле, вопрос интересный. С одной стороны, ситуация похожа на то, что мы видели ранее со стримом. С другой стороны, ситуация _кардинально_ отличается. В случае со стримом кейс, где идет работа далее с невалидным объектом — основной сценарий использования. Тут же — дополнительный (например, из-за ошибки пытаемся залочить рекурсивно). Разница в том, что в одном случае это отлавливается сразу же, и не приводит к фатальному изменению состояния объекта (ну или не должна приводить). Тут же — приводит, очевидным образом, сделав 2 раза лок (первый раз неявно, в конструкторе), сделать 2 раза анлок мы уже не сможем, по вполне очевидным причинам
Стоит ли тут использовать исключения или assert? Я, например, не знаю. Мнения?
В общем, вот все это и хочется формализовать
Более того, надо еще учесть и требования к перфомансу. Но это вторично. Главное — не дать проигнорировать фатальную ошибку, ну и не писАть лишнего — как проверок возвращаемых кодов, так и лишних try-catch. И то, и то, на мой взгляд, вполне сравнимое зло.
J>>>О, я придумал формальный критерий: всегда должны использоваться исключения, потому что они обеспечивают гарантию непродолжения в случае ошибки, не давая програмисту неявно ошибиться.
AS>>Удобство использования страдает. Пример — тот же File. J>Да ладно, где оно там так уж фатально страдает?
Например, проходимся по списку файлов, пробуем открыть. Если нет — ничего страшного, пробуем следующий. На мой взгляд, именно для таких кейсов в файла помимо конструктора должна быть еще и open. Кроме того, open может возвращать расширенный статус — например, успешность + то, что файл уже существовал. Это позволяет во многих случаях _значительно_ упростить логику за счет атомарности такой проверки. Из того же конструктора такой функциональности нормальным способом не добиться.
J>В примере с перебором файла разницы практически никакой.
AS>>В результате разработчик будет делать как ему удобнее — не проверять коды. Плюсом придется писать кучу апгрейдов из одного интерфейса в другой...
J>Имхо, удобнее как раз с исключениями — ничего явно не надо проверять и бояться, что что-то забыл проверить и пытаешься записать в неоткрытый файл (например).
J>Еще раз, идея следующая — коды ошибок должны рассмативаться как оптимизация (да, и еще один случай забыл — когда пишем небросающий код для обеспечения безопасности исключений).
Здравствуйте, Andrew S, Вы писали:
J>>>>О, я придумал формальный критерий: всегда должны использоваться исключения, потому что они обеспечивают гарантию непродолжения в случае ошибки, не давая програмисту неявно ошибиться.
AS>>>Удобство использования страдает. Пример — тот же File. J>>Да ладно, где оно там так уж фатально страдает?
AS>Например, проходимся по списку файлов, пробуем открыть. Если нет — ничего страшного, пробуем следующий.
А что будет дальше, после успешного открытия? Врядли сохранение объекта file в массиве... Чтение/запись, какая-то обработка — все эти операции также могут кидать исключения. Даже если решить "проблему" c open, вероятность получить в итоге подобный код:
имеется. Собственно вопрос — почему бы сразу не использовать кидающий конструктор для открытия в функции process_file (или во вложенной функции, в случае если process_file только обрабатывает исключения)? Или предполагается переписать process_file на версию, в которой используются некидающие варианты всех вызываемых функций (для избавления от try/catch)?
Re[4]: api design: return code or exception - formal criteri
AS>Правомерно ли тут использование именно исключения, а не ассерта? На самом деле, вопрос интересный. С одной стороны, ситуация похожа на то, что мы видели ранее со стримом. С другой стороны, ситуация _кардинально_ отличается. В случае со стримом кейс, где идет работа далее с невалидным объектом — основной сценарий использования. Тут же — дополнительный (например, из-за ошибки пытаемся залочить рекурсивно). Разница в том, что в одном случае это отлавливается сразу же, и не приводит к фатальному изменению состояния объекта (ну или не должна приводить). Тут же — приводит, очевидным образом, сделав 2 раза лок (первый раз неявно, в конструкторе), сделать 2 раза анлок мы уже не сможем, по вполне очевидным причинам AS>Стоит ли тут использовать исключения или assert? Я, например, не знаю. Мнения?
Конкретно здесь, если рассматривать только unique_lock — assert, т.к. очевидно нарушение предусловия: owns_lock() == false. + есть метод для проверки(owns_lock), + еще некоторые аргументы. Но Lockable Concept (к которой относится lock) не содежит функции owns_lock, поэтому предусловия невозможно обобщить в общем случае. Скорее всего так сделано для дальнейшего расширения набора lock types.
Lockable Concept
The Lockable concept models exclusive ownership. A type that implements the Lockable concept shall provide the following member functions:
void lock();
bool try_lock();
void unlock();
void lock()
Effects:
The current thread blocks until ownership can be obtained for the current thread.
Postcondition:
The current thread owns *this.
Throws:
boost::thread_resource_error if an error occurs.
При такой формулировке (общей для всех lock types), с отсутствующими предусловиями — возбуждение исключения единственно верный вариант.
Re[2]: api design: return code or exception - formal criteri
Здравствуйте, slava_phirsov, Вы писали:
_>Почитал-почитал, и понял, почему разработчики известных мне библиотек так осторожно относятся к использованию исключений.
_>P.S. Хотя, возможно, эта книга устарела? Написана 13 лет назад, а, как мне снисходительно пояснил на собеседовании один "молодой, да ранний" гуру вкуривший непосредственно стандарт, "С тех пор C++ радикально изменился".
ИМХО книга отнюдь не устарела.
C++ конечно изменился, но исключения были и тогда, а реализация стандартом не определяется.
Что изменилось, так это аппаратное обеспечение — компы стали пошустрее.
Но всё равно в гэйм деве зачастую исключения отключают нафиг — производительность страдает.
Re[4]: api design: return code or exception - formal criteri
Здравствуйте, jazzer, Вы писали:
J>К чему я и клоню: разработка без исключений — это оптимизация.
Не согласен. Это просто другой подход к разработке. Где-то выше уже поминался iostream, в котором исключения не используются, а используются флаги. ИМХО, в C++ надо десять раз подумать, прежде чем кидаться исключениями. Слышал такой аргумент: "А если в клиентском коде не обрабатываются коды ошибок?". ОК, а если автор клиентского кода засунет функцию генерирующую исключение в try-catch с пустым блоком catch — лучше будет? Ну да, это немного потруднее чем просто проигнорировать код ошибки. Но для копипэйстных кодеров (нормальный-то кодер ИМХО не станет игнорировать код ошибки)- не проблема, и лично я такую стратегию наблюдал неоднократно.
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Re[5]: api design: return code or exception - formal criteri
ЮЖ>Effects:
ЮЖ>The current thread blocks until ownership can be obtained for the current thread.
ЮЖ>Postcondition:
ЮЖ>The current thread owns *this.
ЮЖ>Throws:
ЮЖ>boost::thread_resource_error if an error occurs.
ЮЖ>При такой формулировке (общей для всех lock types), с отсутствующими предусловиями — возбуждение исключения единственно верный вариант.
Не согласен. Приведенная формулировка концепта никоим образом не влияет на то, что именно должен делать lock холдер в случае нарушения _своего_ предусловия. Оунер может быть и рекурсивным. Assert ровно такое же средство уведомления о нарушении контракта, как и исключение. Вот только объект lock холдер после него в релизе останется в невалидном состоянии. Т.е. тут совсем другая ситуация, чем в случае с handle holder (ака File), который даже после нарушения контракта все равно останется валидным.
J>>>>>О, я придумал формальный критерий: всегда должны использоваться исключения, потому что они обеспечивают гарантию непродолжения в случае ошибки, не давая програмисту неявно ошибиться.
AS>>>>Удобство использования страдает. Пример — тот же File. J>>>Да ладно, где оно там так уж фатально страдает?
AS>>Например, проходимся по списку файлов, пробуем открыть. Если нет — ничего страшного, пробуем следующий.
ЮЖ>А что будет дальше, после успешного открытия? Врядли сохранение объекта file в массиве... Чтение/запись, какая-то обработка — все эти операции также могут кидать исключения. Даже если решить "проблему" c open, вероятность получить в итоге подобный код:
После этого может быть все что угодно. Например, инициация асинхронной операции, где, как вы понимаете, исключений быть не может.
В общем, не стОит упрощать — File слишком сложный объект, чтобы так просто описать все его юзе кейсы.
Здравствуйте, slava_phirsov, Вы писали:
_>Здравствуйте, jazzer, Вы писали:
J>>К чему я и клоню: разработка без исключений — это оптимизация.
_>Не согласен. Это просто другой подход к разработке. Где-то выше уже поминался iostream, в котором исключения не используются, а используются флаги.
Во-первых, ты мешаешь все в кучу.
Там флаги состояния потока (которые устанавливаются функцией setstate!). А это значит, что реализуется конечный автомат, и к исключениям это никакого отношения не имеет.
А вот как сигнализировать о том, что пользователь пытается выполнить операцию, которая недопустима для текущего состояния или переводит поток в плохое состояние (failbit) — тут можно использовать и исключения, и коды.
Во-вторых, неплохо бы такие утверждения проверять и не верить им на слово.
Открываем стандарт и чуть ли не в каждом абзаце видим:
If ......, calls setstate(failbit), which may throw ios_base::failure (27.4.4.3).
что же означает may throw?
Это означает, что iostream можно сконфигурировать как для бросания исключений, так и молчаливого возврата.
Смотрим 27.4.4 и видим такую пару функций:
A mask that determines what elements set in rdstate() cause exceptions to be thrown.
_>ИМХО, в C++ надо десять раз подумать, прежде чем кидаться исключениями.
Пока что я не услышал внятной аругментации, по которой отказ от исключений не должен рассматриваться в ряду прочих оптимизаций или телодвижений по поддержке странных платформ.
_>Слышал такой аргумент: "А если в клиентском коде не обрабатываются коды ошибок?". ОК, а если автор клиентского кода засунет функцию генерирующую исключение в try-catch с пустым блоком catch — лучше будет? Ну да, это немного потруднее чем просто проигнорировать код ошибки.
Именно что потруднее, но главное даже не это, главное, что пустой catch очень хорошо виден, и code review такой код просто не пройдет.
_>Но для копипэйстных кодеров (нормальный-то кодер ИМХО не станет игнорировать код ошибки)- не проблема, и лично я такую стратегию наблюдал неоднократно.
Против копипейстных кодеров, кроме code review, вообще приема нет, это диверсанты.
Также я видел море кода нормальных кодеров (включая себя), которые забывали проверять коды ошибок.
Еще раз: проблема кодов ошибок — это их невидимость.
Вызов функции, которая ничего не возвращает, в коде никак не отличается от вызова функции, которая возвращает код ошибки.
Вот это на 4.5 порядка сложнее отследить, чем написанный большими русскими буквами пустой catch.
Ну и обработка кодов ошибок очень сильно захламляет код, в отличие от кода, написанного с исключениями, в котором основная линия исполнения программы (т.е. когда не происходит ошибок) незамусорена и легко читается (== легко поддерживается).
ЮЖ>>Effects:
ЮЖ>>The current thread blocks until ownership can be obtained for the current thread.
ЮЖ>>Postcondition:
ЮЖ>>The current thread owns *this.
ЮЖ>>Throws:
ЮЖ>>boost::thread_resource_error if an error occurs.
ЮЖ>>При такой формулировке (общей для всех lock types), с отсутствующими предусловиями — возбуждение исключения единственно верный вариант.
AS>Не согласен. Приведенная формулировка концепта никоим образом не влияет на то, что именно должен делать lock холдер в случае нарушения _своего_ предусловия.
Очень даже влияет — объект, моделирующий этот концепт, не должен усиливать предусловия (и ослаблять постусловия) для функции lock. unique_lock не имеет "своего" предусловия для ф-ии lock, т.к:
Specializations of boost::unique_lock model the TimedLockable concept if the supplied Lockable type itself models TimedLockable concept (e.g. boost::unique_lock<boost::timed_mutex>), or the Lockable concept otherwise.
Re[5]: api design: return code or exception - formal criteri
Здравствуйте, Quasi, Вы писали:
Q>void f(boost::system::error_code& ec = boost::system::throws);
Q>Нельзя невольно проигнорировать ошибку, исключает необходимость дублирования интерфейса
И исключает возможность привязки по указателю на функцию
Здравствуйте, gear nuke, Вы писали:
GN>Здравствуйте, Andrew S, Вы писали:
AS>>вот такое раздвонение интерфейса.
GN>В последней ревизии немного другой подход: GN>
Здравствуйте, slava_phirsov, Вы писали:
_>Здравствуйте, jazzer, Вы писали:
J>>К чему я и клоню: разработка без исключений — это оптимизация.
_>Не согласен. Это просто другой подход к разработке.
Другой подход — это следствие, причем весьма заметное, т.к. влияет на дизайн в целом, и не редко, в худшую сторону. Как минимум, контракты становятся "толще", и соответственно повышается вероятность ошибки в самом контракте, не говоря уже о реализации и использовании.
Можно услышать такой аргумент против — при использовании исключений приходится отдавать дань за удобство в виде abstraction penalty, а именно — снижение производительности. Этот аргумент может быть актуальным, а может быть и нет. Реальное снижение производительности может быть вызвано неправильным использованием — логика на исключениях, либо использованием в контекстах, в которых стоимость производимых операций одного порядка со стоимостью затрат на поддержку исключений. Оторванные от контекста заявления вроде 'исключения снижают производительность' — не более чем сферокони. Для проверки так это или нет — нужно смотреть конктерную ситуацию. Плюс, адекватное сравнение производительности двух подходов выполнить достаточно сложно.
Так же на нераспростроненность исключений (косвенно это можно назвать 'другим подходом') сказывается соседство с С и наличие огромного количества legacy кода без использования исключений. Некоторые особенности "модульной" системы С++ (осутствие единого, полностью специфицированного ABI), таже не поощеряют исключения.
_>... ИМХО, в C++ надо десять раз подумать, прежде чем кидаться исключениями.
Безусловно, использование исключений требует некоторой дисциплины. Но осязаемых бенефитов в совокупности в результате больше, имхо.
Юрий Жмеренецкий:
ЮЖ>Throws: ЮЖ>boost::thread_resource_error if an error occurs. [/q]
ЮЖ>При такой формулировке (общей для всех lock types), с отсутствующими предусловиями — возбуждение исключения единственно верный вариант.
Какой же он верный, когда boost::lock_error и boost::thread_resource_error — это два несвязанных наследованием исключения? IMHO, тут в любом случае несоответствие концепции получается. Здесь, вероятно, нужна более общая концепция — что-то вроде OnceLockable. Объект, соответствующий концепции Lockable (MultiLockable), мог бы быть использован как OnceLockable, но не наоборот.