LINUX.ORG.RU

exceptions в C++

 ,


5

9

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

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

UPD:
Из любопытного
почитать (стр. 32)
посмотреть

★★★★★

Последнее исправление: morse (всего исправлений: 1)
Ответ на: комментарий от asaw

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

morse ★★★★★
() автор топика
Ответ на: комментарий от RazrFalcon

Нет, я не про них. Насколько же ты воинственен в своём невежестве...

Starting with GCC 3.2, GCC binary conventions for C++ are based on a written, vendor-neutral C++ ABI that was designed to be specific to 64-bit Itanium but also includes generic specifications that apply to any platform. This C++ ABI is also implemented by other compiler vendors on some platforms, notably GNU/Linux and BSD systems.

Если коротко, то на сегодняшний день существует два основных ABI для C++: Itanium ABI и ABI Microsoft.

asaw ★★★★★
()
Последнее исправление: asaw (всего исправлений: 1)
Ответ на: комментарий от RazrFalcon
template <typename R, typename E>
class Result {
public:
  setError(E err) {
    this->err = err;
    error_occurred = true;
  }

  setResult(R res) {
    this->res = res;
    error_occurred = false;
  }

  bool isError() {
    error_checked = true;
    return error_occurred;
  }

  E error() {
    return err;
  }

  R result() {
    assert(error_checked);
    return res;
  }

private:
  R res;
  E err;
  bool error_occurred;
  bool error_checked {false};
};
…
Result<int, string> r = foo(bar);
if (r.isErr()) {
  cerr << r.error();
} else {
  cout << r.result();
}


Концепт такой, допилить только.

evilface ★★
()

Слушайте таварищи. Но вот все же. Помните как в boost asio при асинхронном стиле вызовы функций добавляются в очередь, но непосредственно выполняются эти функции при вызове метода run() у объекта-сервиса. Так вот. Внутри таких функций выбрасывать исключение и потом ловить его за пределами объекта сервиса - это фигня какая-то. В таком случае ведь лучше передать из это функции в вызов следующей функции код завершения данной.

rumgot ★★★★★
()
Ответ на: комментарий от morse

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

Я тебе даю ссылку не на конкретную реализацию, а на ABI, из которого уже много чего понятно.

asaw ★★★★★
()
Ответ на: комментарий от asaw

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

morse ★★★★★
() автор топика
Ответ на: комментарий от evilface

Лул. А алгебраические типы тоже можно реализовать? Удобство растовского Result в комбинации с растовским enum.

RazrFalcon ★★★★★
()
Ответ на: комментарий от evilface

Создавать по новому одноразовому объекту на каждый вызов функции? Да еще и через шаблоны? А месье знает толк...

morse ★★★★★
() автор топика
Ответ на: комментарий от morse

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

asaw ★★★★★
()
Ответ на: комментарий от morse

Создавать по новому одноразовому объекту на каждый вызов функции? Да еще и через шаблоны? А месье знает толк...

Он всё правильно знает. В C++ объекты замечательно умеют жить на стеке, в отличие от жаб и шарпов. Поэтому создание такого объекта ничем не отличается от помещения на стек какой-нибудь переменной для вызова функции (то же самое - с возвратом).

asaw ★★★★★
()
Ответ на: комментарий от asaw

Теперь понял.

Ну и:

Реализаций компиляторов C++ - вагон и маленькая тележка.
на сегодняшний день существует два основных ABI для C++: Itanium ABI и ABI Microsoft.

RazrFalcon ★★★★★
()
Ответ на: комментарий от morse

Создавать по новому одноразовому объекту на каждый вызов функции?

Дык дёшево же.

Да еще и через шаблоны?

Еще скажите, что шаблоны замедляют код.

RazrFalcon ★★★★★
()
Ответ на: комментарий от asaw

Нет, я не про них. Насколько же ты воинственен в своём невежестве...

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

anonymous
()
Ответ на: комментарий от morse

Скажу - «усложняют».

идите в раст и навсегда забудьте про C++, если шаблоны вам что-то «усложняют»

anonymous
()
Ответ на: комментарий от morse

Создавать по новому одноразовому объекту на каждый вызов функции? Да еще и через шаблоны? А месье знает толк...

Вернуть Result<int, int> == вернуть (в среднем) байт шестнадцать. Как обычную структуру. Вообще проблем никаких. Выделять динамически память для этого не надо.

evilface ★★
()
Ответ на: комментарий от anonymous

да, мы уже проходили это с функциями типа atoi

Как там, в 70-х?

Думаю вам надо пойти из крестов в rust и не морочить никому голову

А мы думаем, что на крестах тоже можно изобразить ADT и пользоваться им для возврата ошибок.

tailgunner ★★★★★
()
Ответ на: комментарий от asaw

В C++ объекты замечательно умеют жить на стеке, в отличие от жаб и шарпов.

Справедливости ради, в шарпе есть struct, который умеет жить на стеке.

evilface ★★
()

Из почитать об устройстве помню такое и такое.

И есть видео по их применению.

Вообще они обычно бесплатны для производительности, если исключение не происходит, и «дорогие», если происходит, но в том смысле, что дороже вызова виртуальной функции (как сравнительно тяжёлой операции в C++ по сравнению с каким-нибудь обычным вызовом или инкрементом переменной).

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

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Из почитать об устройстве помню такое и такое.

Второе это перевод скинутого мной на сообщение выше.

utf8nowhere ★★★
()
Ответ на: комментарий от tailgunner

допилить только.

Не-не, это лучше просто закопать.

Ну или так. В любом случае, это просто идея к тому, чем можно заменить растовский std::result.

evilface ★★
()
Ответ на: комментарий от evilface

Вернуть Result<int, int> == вернуть (в среднем) байт шестнадцать.

Кстати, ровно то же, что и в C++ вернуть std::pair<int,int>

anonymous
()
Ответ на: комментарий от anonymous

Именно поэтому это плохой пример. Возвращать нужно ADT.

tailgunner ★★★★★
()
Ответ на: комментарий от evilface

для нормальной работы с кодами возврата нужен ПМ

{ok, one} = atoi(«1»)

а так и костыль вида errno сойдет, оставайтесь в криокамерах, без проблем

anonymous
()

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

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

То что наублюдили в rust и go, и представленный тут Result<> недалеко от этого не ушли. Всё также надо писать проверки после каждого вызова, и обкостыливание их макросами типа растовского try! и go'шного аналога ничего не меняет. Всё также тормозно, всё также оганичего. Всё также можно забыть правильно обработать ошибку. Всё также нужно маппить ошибки из разных namespace'ов. Всё ещё нужно заранее продумывать архитектуру ошибок заранее, чтобы потом всю обработку не переписывать.

Исключения, в то же время, позволяют не задумываться об ошибках и просто писать код, при этом никакая ошибка не будет пропущена. Можно не обрабатывать их вообще, если не нужно, и получить завершение приложения при ошибке. Можно обработать их на любом удобном уровне иерархии вызовов. Хочешь - обработай по месту возникновения. Хочешь - обработай при запуске сложной операции. Далее, исключение сложный объект куда можно добавить любую информацию, в том числе и другое исключение, и получить красивую цепочку причин ошибки. Всё это zero-cost, конструируется и разматывается только при возникновении исключения, в противном случае никаких runtime накладных расходов не несёт. Исключения наследуются, причём иерархию можно менять в любое время и не нужно менять код по всему проекту.

В общем, в современном мире ничего лучше исключений не придумали. К сожалению, в C++ ни всё-таки ограничены. Ни конструктор копирования, ни what() не могут кидать.

Единственный минус их в C++ - ни копирование std::exception, ни what() не могут кидать, значит внутри на самом деле нельзя хранить сколь-либо сложных объектов.

slovazap ★★★★★
()
Ответ на: комментарий от anonymous

для нормальной работы с кодами возврата нужен ПМ

Да.

{ok, one} = atoi(«1»)

...но это не ПМ.

tailgunner ★★★★★
()
Ответ на: комментарий от slovazap

Всё очень плохо. Особенно:

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

Это и есть основная проблема исключений - неизвестно что придёт.

RazrFalcon ★★★★★
()
Ответ на: комментарий от Legioner

В плюсах нет нормального Result как в раст, поэтому исключения спасают.

Что мешает сделать?

Ничто, в общем.

tailgunner ★★★★★
()

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

В двух словах: «не применяй». И не потому что они неэффективны — реализация вполне себе на уровне, а потому что исключения делают твой код и твои API крайне проблематичными для понимания.

Касательно реализации на этих ваших линуксах: очевидно, что stack unwinding можно делать находясь вне этого стека или на самом его «дне» — без понятия, какой именно путь был выбран, но, в принципе, оба варианта можно считать весьма «легкими» с точки зрения потребления дополнительной памяти.

Также полезно знать, что исключения в g++ переделывались несколько раз и в конце-концов была выбрана стратегия «за все платит throw» — то есть, можешь городить любой огород из try и ничего не будет тормозить, пока реально не начнутся бросания исключений. Поэтому используй исключения только для действительно исключительных ситуаций. Бросать исключение на EIO — ok, на timeout — может быть ок, на EAGAIN — странно, на EINTR — плохая идея.

Поскольку единственное, что у тебя есть — это стек вызовов (т. е. набор instruction pointer-ов), значит для разматывания необходима таблица трансляции «instruction pointer => правила для вычисления адресов объекта и вызываемых деструкторов». Получается, что при пробросе исключения, требуется сделать lookup в этй таблице, а затем интерпретировать некий «псевдокод», зашитый в ней, а потом еще позвать энное кол-во деструкторов.

Помимо stack unwinding нужно также подобрать правильный catch — это тоже чего-то да стоит. Думаю, можно считать, что каждый catch — это что-то вроде особенного dynamic_cast, а это rtti и считается не самой дешевой операцией.

kawaii_neko ★★★★
()
Ответ на: комментарий от anonymous

atoi

defective by design, но простой интерфейс идеально подходит для случаев, когда в строке точно лежит число, либо «0» также является недопустимым результатом.

Ошибку обработки строки никак не получить

Открой для себя strtol.

kawaii_neko ★★★★
()
Ответ на: комментарий от kawaii_neko

Открой для себя strtol.

маладец, вали кодить на С и не морочь людям голову

anonymous
()
Ответ на: комментарий от xaizek

дороже вызова виртуальной функции (как сравнительно тяжёлой операции в C++ по сравнению с каким-нибудь обычным вызовом или инкрементом переменной).

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

Кстати, на постинкременте сложного типа можно словить проседание производительности буквально на ровном месте.

kawaii_neko ★★★★
()
Ответ на: комментарий от RazrFalcon

Отсутствие удобных enum'ов, как в расте.

Что подразумевается под удобными enum-ами? Можно пример кода в Rust-е, который будет неудобен в C++. В C++ разве что паттерн-матчинга нет, но для двух значений можно и без него прекрасно обойтись.

Legioner ★★★★★
()
Ответ на: комментарий от anonymous

Под плюсами в 2017 году понимают что-то не ниже c++11.

Чего? Можно использовать «навороты» из С++17 и не использовать исключения.

DarkEld3r ★★★★★
()
Ответ на: комментарий от RazrFalcon

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

«Код возврата» тоже может быть полиморфный (или какой-нибудь variant/enum).

DarkEld3r ★★★★★
()
Ответ на: комментарий от anonymous

идите в раст и навсегда забудьте про C++, если шаблоны вам что-то «усложняют»

Раст не особо проще плюсов, местами может даже наоборот.

DarkEld3r ★★★★★
()
Ответ на: комментарий от slovazap

Всё также надо писать проверки после каждого вызов

Так это достоинство.

Всё также можно забыть правильно обработать ошибку.

Раскрой тему. Потому что «неправильно обработать» можно вообще всё, что угодно.

Всё ещё нужно заранее продумывать архитектуру ошибок заранее, чтобы потом всю обработку не переписывать.

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

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

Именно и это, конечно, бывает очень удобно, но на этом плюсы заканчиваются.

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

Было бы интересно взглянуть на проект с толковым использованием «цепочки исключений», потому что обрабатывать их ещё сложнее, а для тупой записи в лог все эти навороты не требуются.

DarkEld3r ★★★★★
()
Ответ на: комментарий от RazrFalcon

Всё очень плохо

Раскрывай мысль.

Это и есть основная проблема исключений - неизвестно что придёт

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

slovazap ★★★★★
()
Ответ на: комментарий от DarkEld3r

Из моей практики в 99% случаев достаточно варианта, когда ошибка прерывает выполнение запроса, улетает наверх и в каком-то месте пишется в лог. Например, если это HTTP-сервер, обрабатывающий запросы, в лог записывается стектрейс до места, откуда вылетела ошибка, клиенту возвращается код 500, а сам сервер продолжает работать, как работал. Для этих 99% случаев исключения чрезвычайно удобны, поскольку позволяют не захламлять код бессмысленной обработкой ошибок, а там, где эта обработка действительно бывает нужна — она без проблем пишется. В случае возврата значений приходится во-первых помнить, что их надо обработать (в том же С++ компилятор просто так и не напомнит про это, может быть какая-нибудь прагма есть, но про неё тоже нужно не забыть и в сторонних библиотеках её может и не быть), т.к. если не обработаешь, то скорее всего свалится позже уже с совсем другой ошибкой, маскируя исходную проблему; во-вторых, даже если ты это запомнил и обработал, код начинает пестрить этими обработками и выглядеть неприглядно.

Legioner ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.