Здравствуйте, Pzz, Вы писали:
Pzz>Этим исключениям хорошо бы как-то законодательно синтаксически ограничить дальность полета. Потому что когда в ваш высокоуровневый код, работающий в терминах высокоуровневых абстракций, откуда-нибудь с самого нижнего уровня прилетит исключение про то, чего вы в своем высокоуровневом коде сказать-то и не можете, то что вы с ним будете делать? Выдадите пользователю ошибку с шешнадцетиричным номером и невнятной диагностикой? То-то пользователь будет счастлив.
Если ты не знаешь что делать с ошибкой, прибивай программу. Это намного лучше чем считать погоду в Африке.
Низкоуровневое исключение — это почти всегда открытые открытые файлы, захваченные мьютексы/критические секции, выделенная память и т.д, но самое главное — инконсистентное состояние программы.
Всё сказанное выше — личное мнение, если не указано обратное.
Когда-то этот вопрос уже здесь обсуждался, причём именно для случая C++ (в контексте запрета исключений в гугловских руководствах). Я тогда подробно (с подтверждающими примерами) изложил свою точку зрения. Могу повторить тезисно в этой темке.
Главное преимущество исключений в том, что они позволяют без напряжения для программиста и оптимально по быстродействию (как раз тесты из данной темки это подтверждают) передавать ошибку через множество уровней стека вызова. Так вот, на мой взгляд на практике нормальная обработка ошибок всегда происходит практически на следующем уровне вложенности, а не где-то далеко наверху. Происходит это по уже упоминаемой тут причине — иначе не получится выдавать адекватные сообщения об ошибках. Т.е. даже если мы сделаем идеальный для системы исключений случай с единственной на всю программу точкой реакции на ошибки (грубо говоря один try/catch на всю программу), то нам всё равно придётся по несколько раз переупаковывать их по ходу дела (хотя бы потому, что одинаковые низкоуровневые исключения в разных модулях означают разные вещи). Т.е. в итоге мы всё равно приходим к кучей блоков try/catch раскиданных по всей программе, причём как показывает практика, чаще всего они будут прямо вокруг функции кидающей исключение. А это полностью убирает все преимущества исключений. Более того, при таком раскладе они становятся более многословным и при этом менее явным способом работы.
Здравствуйте, Vain, Вы писали:
V>Здравствуйте, uncommon, Вы писали:
V>>>непонятно только, как исключения в этом случае будут быстрее кодов ошибок? U>>Коды ошибок всегда проверять нужно, произошла ошибка или нет. А исключения задействуются, только когда произошла ошибка. V>Ну это не совсем так, при входе в try блок ведь чтото должно делаться, не так ли? Единственный вопрос, быстрее ли оно обычного условия в случае проверки кода возврата.
Нет, ничего не надо делать, это обычный scope с толпой деструкторов в конце. И исключение — это по сути табличное goto куда-то в середину этой пачки деструкторов, в зависимости от того, в какой строчке вылетело исключение (чтоб убить только то, что успело создаться к этому моменту). И место goto вычисляется только при размотке стека в случае исключения.
Здравствуйте, alex_public, Вы писали:
_>Т.е. в итоге мы всё равно приходим к кучей блоков try/catch раскиданных по всей программе, причём как показывает практика, чаще всего они будут прямо вокруг функции кидающей исключение. А это полностью убирает все преимущества исключений. Более того, при таком раскладе они становятся более многословным и при этом менее явным способом работы.
+1
Во всех мало-мальски крупных системах, которые я видел (не миллионы строк, но сотни тысяч) — именно так и есть. Причем я говорю о софте разного типа (записи судебных заседаний, ГИС, программы учета), созданном в разных компаниях (в т.ч. занимающихся исключительно разработкой ПО).
Здравствуйте, enji, Вы писали:
E>Здравствуйте, Miroff, Вы писали:
M>>Запретить null как класс. Вместо него использовать либо Option, либо вообще ничего не использовать.
E>И кстати — как его запретить? Вообще запретить простые ссылки? Т.е. везде только Option?
Запретить неинициализированные ссылки и ключевое слово null.
Т.е.
User user; // error
return null; // error
foo(null); //error
E>И во что тогда превратится такое: obj1.calc().doIt().getSome() ?
За такой код в приличном обществе принято бить канделябром потому что это нарушение DSP.
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, vsb, Вы писали:
_>Когда-то этот вопрос уже здесь обсуждался, причём именно для случая C++ (в контексте запрета исключений в гугловских руководствах). Я тогда подробно (с подтверждающими примерами) изложил свою точку зрения. Могу повторить тезисно в этой темке.
_>Главное преимущество исключений в том, что они позволяют без напряжения для программиста и оптимально по быстродействию (как раз тесты из данной темки это подтверждают) передавать ошибку через множество уровней стека вызова. Так вот, на мой взгляд на практике нормальная обработка ошибок всегда происходит практически на следующем уровне вложенности, а не где-то далеко наверху. Происходит это по уже упоминаемой тут причине — иначе не получится выдавать адекватные сообщения об ошибках. Т.е. даже если мы сделаем идеальный для системы исключений случай с единственной на всю программу точкой реакции на ошибки (грубо говоря один try/catch на всю программу), то нам всё равно придётся по несколько раз переупаковывать их по ходу дела (хотя бы потому, что одинаковые низкоуровневые исключения в разных модулях означают разные вещи). Т.е. в итоге мы всё равно приходим к кучей блоков try/catch раскиданных по всей программе, причём как показывает практика, чаще всего они будут прямо вокруг функции кидающей исключение. А это полностью убирает все преимущества исключений. Более того, при таком раскладе они становятся более многословным и при этом менее явным способом работы.
Все эти промежуточные try/catch не добавляют ничего, если исключение не было брошено.
Суть в том, чтоб оптимизировать сценарий, когда исключения не летят.
А если летят — то пофиг, насколько медленно это делается, что там как переупаковывается и т.д.
Здравствуйте, Miroff, Вы писали:
E>>И во что тогда превратится такое: obj1.calc().doIt().getSome() ?
M>За такой код в приличном обществе принято бить канделябром потому что это нарушение DSP.
Напомни плиз, как это DSP расшифровывается. А то идею помню (что-то там типа не более двух ссылок в цепочке, иначе заворачивать в методы) и юзаю давно на автомате, а нагуглить не могу.
Здравствуйте, dimgel, Вы писали:
D>Напомни плиз, как это DSP расшифровывается. А то идею помню (что-то там типа не более двух ссылок в цепочке, иначе заворачивать в методы) и юзаю давно на автомате, а нагуглить не могу.
Здравствуйте, jazzer, Вы писали:
V>>Ну это не совсем так, при входе в try блок ведь чтото должно делаться, не так ли? Единственный вопрос, быстрее ли оно обычного условия в случае проверки кода возврата. J>Нет, ничего не надо делать, это обычный scope с толпой деструкторов в конце.
ну значит расходы на вход в скоп:
Здравствуйте, jazzer, Вы писали:
J>Все эти промежуточные try/catch не добавляют ничего, если исключение не было брошено. J>Суть в том, чтоб оптимизировать сценарий, когда исключения не летят. J>А если летят — то пофиг, насколько медленно это делается, что там как переупаковывается и т.д.
Ну вообще говоря это зависит от реализации, sjlj/seh/dwarf и т.п. Но в данном случае я говорил не об оптимизации, а об удобстве программиста. Ведь если ловить исключения непосредственно над функцией кидающей их, это получается заметно более громоздко и неудобно. Т.е. сама теоретическая идея исключений очень хороша, но на практике весьма редко получается использовать её преимущества (более того, при указанном выше раскладе, она скорее во вред уже идёт).
Здравствуйте, dimgel, Вы писали:
D>Напомни плиз, как это DSP расшифровывается. А то идею помню (что-то там типа не более двух ссылок в цепочке, иначе заворачивать в методы) и юзаю давно на автомате, а нагуглить не могу.
(зевает) Я, оказывается, и идею не помнил: не лезть через голову контрагента, а длина цепочки — уже следствие. Хотя юзал на автопилоте всё равно правильно, в подсознание вбил однако.
Здравствуйте, Miroff, Вы писали:
M>Здравствуйте, dimgel, Вы писали:
D>>Напомни плиз, как это DSP расшифровывается. А то идею помню (что-то там типа не более двух ссылок в цепочке, иначе заворачивать в методы) и юзаю давно на автомате, а нагуглить не могу.
M> Извиняюсь, LoD конечно.
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, jazzer, Вы писали:
J>>Все эти промежуточные try/catch не добавляют ничего, если исключение не было брошено. J>>Суть в том, чтоб оптимизировать сценарий, когда исключения не летят. J>>А если летят — то пофиг, насколько медленно это делается, что там как переупаковывается и т.д.
_>Ну вообще говоря это зависит от реализации, sjlj/seh/dwarf и т.п. Но в данном случае я говорил не об оптимизации, а об удобстве программиста. Ведь если ловить исключения непосредственно над функцией кидающей их, это получается заметно более громоздко и неудобно.
А можно продемонстрировать громоздкость и неудобность на примере?
Вот чтобы передавалась наверх одна и та же информация, только один раз — кодом возврата, а другой раз — исключением.
Ну чтоб было видно, что исключения сливают, на примере, так сказать.
Скажем, нужно открыть 4 сокета, прочитав параметры из разных конфигов. Мы делаем вперемежку 4 вызова функции (или конструктора обертки) open_file, которая может бросить исключение EFileOpen(filename,errno), и 4 вызова функции open_socket, которая может бросить исключение ESocketOpen(sourceIP,sourcePort,targetIP,targetPort,errno).
И вылетевшее исключение переупаковываются в, скажем, EEnvSetup(username).
Как ты понимаешь, c исключениями и переупаковкой код будет примерно таким:
try {
auto f1 = open_file(get_profile_dir(username)+"file1");
s1_ = open_socket(f1); // читаем из конфигаauto f2 = open_file(get_profile_dir(username)+"file2");
s2_ = open_socket(f2); // читаем из конфигаauto f3 = open_file(get_profile_dir(username)+"file3");
s3_ = open_socket(f3); // читаем из конфигаauto f4 = open_file(get_profile_dir(username)+"file4");
s4_ = open_socket(f4); // читаем из конфига
}
catch(...) {
std::throw_with_nested( EEnvSetup(username) );
}
_>Т.е. сама теоретическая идея исключений очень хороша, но на практике весьма редко получается использовать её преимущества (более того, при указанном выше раскладе, она скорее во вред уже идёт).
Ну, моя практика от твоей отличается, видимо.
Я вижу сплошные удобства в своем коде, и каждый вызов каждой функции в отдельный try/catch не заворачиваю.
Здравствуйте, jazzer, Вы писали:
J>А можно продемонстрировать громоздкость и неудобность на примере? J>Вот чтобы передавалась наверх одна и та же информация, только один раз — кодом возврата, а другой раз — исключением. J>Ну чтоб было видно, что исключения сливают, на примере, так сказать.
(и там дальше по ветке основное будет) можно глянуть.
J>Скажем, нужно открыть 4 сокета, прочитав параметры из разных конфигов. Мы делаем вперемежку 4 вызова функции (или конструктора обертки) open_file, которая может бросить исключение EFileOpen(filename,errno), и 4 вызова функции open_socket, которая может бросить исключение ESocketOpen(sourceIP,sourcePort,targetIP,targetPort,errno). J>И вылетевшее исключение переупаковываются в, скажем, EEnvSetup(username).
Я же говорю, проблемы у исключений не в теории, а на практике. Конкретно на практике мне не приходилось писать код с последовательным открытием 4-х сокетов (причём так что проблемы с одним отменяют попытки с остальными) в одной функции. Но зато вполне частенько встречался код вида
auto f1 = open_file(get_profile(username));
if(!f1) f1=open_file(get_default_profile());
s1_ = open_socket(f1); // читаем из конфига
Понимаешь что получится, в случае переписывания такого варианта твоего кода на исключениях? )
J>Ну, моя практика от твоей отличается, видимо. J>Я вижу сплошные удобства в своем коде, и каждый вызов каждой функции в отдельный try/catch не заворачиваю.
Я говорил не про оборачивание каждого вызова, а про перехват исключений на следующем уровне (а не где-то высоко). Что ты как раз и продемонстрировал в своём примере. Но и оборачивание отдельных функций тоже возможно, если требуется продолжить нормальное исполнение программы после неудачного вызова функции.
Здравствуйте, LaptevVV, Вы писали:
LVV>Одна из серьезных проблем, которую практически не поминают — это парадигма обработки исключений. LVV>И вот получается, что в одной проге смешаны как минимум две, а то и три парадигмы: ООП+событийное (и очень часто процедурное).
О! Ребята начали чего-то подозревать! Я, помнится, спрашивал тут когда-то: "а как вы собираетесь реагировать на исключения, где перехватывать (если собираетесь перехватывать)". Исключальщики меня только об[какали], типа, у нас все тип-топ, транзакционная семантика, и вообще ты не в теме, учи матчасть.
LVV>И если нормальную работу худо-бедно проектируют, то аварии не проектируют совсем.
Да ладно не проектируют, их и не тестируют совсем — вот беда! За исключением (пардон за каламбур) тривиальных случаев: неправильный аргумент -> функция кидает исключение. И я даже с трудом себе представляю, а как можно написать юнит-тесты для кода, обрабатывающего аварии.
LVV>Надо отметить, что в новом стандарте управляемость исключений СУЩЕСТВЕННО улучшилась. LVV>Текущее исключение можно явным образом сохранить, передать в нужную функцию, заново кинуть в нужном месте.
Но вот спецификации исключений, вместо того, чтобы довести до ума (== до уровня Java) предпочли выпилить, чтобы не париться Уж тут либо полноценные исключения со спецификациями и (может быть) с ограничением распространения по стеку вызовов, по области видимости (например, исключение является внутренним для данного класса), либо ну их нафиг.
Нет, когда Страуструп в D&E написал, что исключения в C++ — это первый опыт комитетской разработки, я не удивился ничуть. "Верблюд — это скаковая лошадь, спроектированная комитетом по стандартизации"(c)
Люди! Люди, смотрите, я сошел с ума! Люди! Возлюбите друг друга! (вы чувствуете, какой бред?)
Здравствуйте, alex_public, Вы писали:
_>Здравствуйте, jazzer, Вы писали:
J>>А можно продемонстрировать громоздкость и неудобность на примере? J>>Вот чтобы передавалась наверх одна и та же информация, только один раз — кодом возврата, а другой раз — исключением. J>>Ну чтоб было видно, что исключения сливают, на примере, так сказать.
_>Обсуждалось это всё уже давным давно) Причём и вместо с тобой. ))) Например здесь http://rsdn.ru/forum/cpp/4623107?tree=tree
(и там дальше по ветке основное будет) можно глянуть.
Ну глянул. Моя позиция не изменилась. Ты там, вроде, мне не возражал.
J>>Скажем, нужно открыть 4 сокета, прочитав параметры из разных конфигов. Мы делаем вперемежку 4 вызова функции (или конструктора обертки) open_file, которая может бросить исключение EFileOpen(filename,errno), и 4 вызова функции open_socket, которая может бросить исключение ESocketOpen(sourceIP,sourcePort,targetIP,targetPort,errno). J>>И вылетевшее исключение переупаковываются в, скажем, EEnvSetup(username).
_>Я же говорю, проблемы у исключений не в теории, а на практике. Конкретно на практике мне не приходилось писать код с последовательным открытием 4-х сокетов (причём так что проблемы с одним отменяют попытки с остальными) в одной функции.
А мне вот приходится регулярно. Там сокеты, правда, упрятаны внутрь соответствующих транспортов/протоколов, но это роли не играет — они все создаются разом и все должны работать.
_>Но зато вполне частенько встречался код вида _>
_>auto f1 = open_file(get_profile(username));
_>if(!f1) f1=open_file(get_default_profile());
_>s1_ = open_socket(f1); // читаем из конфига
_>
_>Понимаешь что получится, в случае переписывания такого варианта твоего кода на исключениях? )
Во-первых, в таких случаях используются функции типа can_open_file(filename), но даже в твоей формулировке:
try {
try {
f1 = open_file(get_profile(username));
} catch (...) {
f1 = open_file(get_default_profile());
}
s1_ = open_socket(f1); // читаем из конфига
}
catch(...) { std::throw_with_nested( EEnvSetup(username) ); }
а в реальном коде, даже если у нас в API только исключения, будет отдельная функция
try {
f1 = get_profile_or_default(username);
s1_ = open_socket(f1); // читаем из конфига
}
catch(...) { std::throw_with_nested( EEnvSetup(username) ); }
Ну а теперь изобрази свой замечательный вариант на кодах возврата. С донесением всей необходимой информации на верхний уровень: если не открылся файл, то какой и почему, если сокет, то какой и почему.
Жду-пожду.
J>>Ну, моя практика от твоей отличается, видимо. J>>Я вижу сплошные удобства в своем коде, и каждый вызов каждой функции в отдельный try/catch не заворачиваю.
_>Я говорил не про оборачивание каждого вызова, а про перехват исключений на следующем уровне (а не где-то высоко). Что ты как раз и продемонстрировал в своём примере.
Да, продемонстрировал. И как, это было так ужасно, что ты демонстрацию стер с глаз долой? Целая одна строчка с std::throw_with_nested?
_>Но и оборачивание отдельных функций тоже возможно, если требуется продолжить нормальное исполнение программы после неудачного вызова функции.
Да, на верхнем уровне, скажем, в обработчике кнопок меню: прилетело исключение — рассказал пользователю, что облом (с показом по требованию всей собранной исключениями через throw_with_nested информации), и работаешь себе дальше.
Потому что если можно продолжить нормальное исполнение программы после неудачного вызова функции на низком уровне, то это не исключительная ситуация по определению, правда?
(и там дальше по ветке основное будет) можно глянуть. J>Ну глянул. Моя позиция не изменилась. Ты там, вроде, мне не возражал.
Ну так консенсус (во всяком случае у меня с собеседникам) там сложился на формулировке "исключения хорошо подходят для обработки критических (т.е. после которых программа скорее всего прервёт исполнение) ошибок, а не всех видов ошибок вообще". Причём тут есть ещё хитрый нюанс — ошибки во многих API (типа того же открытия файла) могут быть как критическими, так и нет, в зависимости от контекста использования. Получается что по идее надо бы иметь по два вида API в таких библиотеках. Но Boost.Asio — это скорее исключение, чем правило...
В общем для большинства случаев (если не трогать таки вещи как скажем new) мне кажется, что лучше делать интерфейс без исключений. Ну или тогда уж оба варианта, как в Asio.
J>Во-первых, в таких случаях используются функции типа can_open_file(filename), но даже в твоей формулировке: J>
Ну так разве это всё не ужас, по сравнению с тем моим кодом в 3 строчки? )
J>Ну а теперь изобрази свой замечательный вариант на кодах возврата. С донесением всей необходимой информации на верхний уровень: если не открылся файл, то какой и почему, если сокет, то какой и почему. J>Жду-пожду.
Так а зачем выносить эту информацию на верхний уровень? ) Пользователю будешь это показывать? ))) Как раз это и обсуждалось в той старой темке...
J>Да, продемонстрировал. И как, это было так ужасно, что ты демонстрацию стер с глаз долой? Целая одна строчка с std::throw_with_nested?
Не о том речь. Просто у исключений есть очевидные преимущества. Но они проявляются как раз для случая проброса через длинный стек вызова. Т.е. в идеальной модели с исключениями мы имеет вообще один единственный блок с try/catch на всю программу. Так вот, я как раз и утверждаю, что на практике такая модель может получиться только в очень специфических случаях. И ты только подтвердил это своим примером. Да, и я не говорю, что этот код сильно ужасен. Просто при такой схеме все преимущества исключений пропадают. А недостатки (например неявность ошибок) остаются.
_>>Но и оборачивание отдельных функций тоже возможно, если требуется продолжить нормальное исполнение программы после неудачного вызова функции. J>Да, на верхнем уровне, скажем, в обработчике кнопок меню: прилетело исключение — рассказал пользователю, что облом (с показом по требованию всей собранной исключениями через throw_with_nested информации), и работаешь себе дальше. J>Потому что если можно продолжить нормальное исполнение программы после неудачного вызова функции на низком уровне, то это не исключительная ситуация по определению, правда?
Да, и тогда применять исключения не надо. Собственно о том и речь, что исключения хорошо подходят для решения только небольшой части общей задачи обработки ошибок. В отличие от тех же кодов возврата, которые подходят везде.