Версия для печати
Нажмите сюда для просмотра этой темы в оригинальном формате |
Форум на Исходниках.RU > Holy Wars > C++ RAII/exceptions vs Golang defer/panic/recover |
Автор: korvin 22.08.15, 08:11 |
Я, конечно, помню, что исключения в деструкторах --- это плохо, но всякое ж бывает (возможно, кроме этого, я что-то неправильно написал в C++ коде): <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> #include <iostream> #include <stdexcept> using namespace std; class Resource { private: string _name; bool _exn; public: Resource(string name, bool exn) { _exn = exn; _name = name; cout << "Open " << _name << endl; } virtual ~Resource() { cout << "Close " << _name << endl; if (_exn) throw runtime_error("EXCEPTION: " + _name); } }; void test() { Resource a("A", false); Resource b("B", true); cout << "test" << endl; } int main() { try { test(); } catch (runtime_error& e) { cout << "CATCH: " << e.what() << endl; } return 0; } stdout: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Open A Open B test Close B stderr: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> terminate called after throwing an instance of 'std::runtime_error' what(): EXCEPTION: B --- http://ideone.com/zH6e0Q Golang: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> package main import "fmt" type Resource struct { name string exn bool } func OpenResource(name string, exn bool) (*Resource, error) { r := &Resource{name, exn} fmt.Println("Open", name) return r, nil } func (r *Resource) Close() error { fmt.Println("Close", r.name) if r.exn { panic("EXCEPTION: Close " + r.name) } return nil } func test() error { a, err := OpenResource("A", false) if err != nil { return err } defer a.Close() b, err := OpenResource("B", true) if err != nil { return err } defer b.Close() fmt.Println("test") return nil } func main() { defer rec() test() } func rec() { if r := recover(); r != nil { fmt.Println("CATCH:", r) } } stdout: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Open A Open B test Close B Close A CATCH: EXCEPTION: Close B --- http://play.golang.org/p/bG-YnPIGX0 |
Автор: D_KEY 22.08.15, 08:22 |
korvin, видимо, понимаешь не до конца. В деструкторах не должно быть исключений. Совсем. Добавлено Ну и да, код не аналогичен Для аналога того, что написано в Go, тебе нужно реализовать defer(в виде некоторого guard'а), в котором дергать метод close ресурса. |
Автор: korvin 22.08.15, 08:54 |
Цитата D_KEY @ korvin, видимо, понимаешь не до конца. В деструкторах не должно быть исключений. Совсем. Но если там довольно сложная логика освобождения ресурса, оно, тем не менее, может произойти, из-за какой-нибудь вызываемой библиотечной функции или аппаратной ошибки, 100% гарантии нет, или как? Цитата D_KEY @ Ну и да, код не аналогичен Для аналога того, что написано в Go, тебе нужно реализовать defer(в виде некоторого guard'а), в котором дергать метод close ресурса. Может и не аналогичен, но, пусть это чуть разные механизмы, но они используются для одной и той же цели и с более-менее схожим смыслом. Раньше это не мешало сравнивать RAII и try/catch/finally например. =) Кстати, как раз недавно наткнулся на реализацию чего-то подобного defer (точнее, документ опубликован ещё до выхода бета-версий Go, так что правильней наоборот, да и вообще, но не суть). Правда там для Java, но всё же. |
Автор: Qraizer 22.08.15, 09:43 |
С этим кодом всё в порядке. В смысле, он, конечно, говнокод, но работать должен, как задумывалось. Что там с вашим g++14, не знаю, а вот Студия, Интел Компилер и g++11: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Исключения в деструкторах в любом случае могут возникать, но не должны покидать их пределов. Если исключение покидает пределы деструктора, это не причина программе падать, это лишь означает, что деструктор не закончил работу, и следовательно объект недоразрушен со всеми вытекающими.Open A Open B test Close B Close A CATCH: EXCEPTION: B Языком не допускаются только такая ситуация, в которой исключение бросается в тот момент, когда предыдущее ещё не поймано. Потому что если возникает такая ситуация, то эти два исключения невозможно однозначно отсортировать для обработки. Это может произойти только в тот момент, когда на пути от throw до catch выполниться ещё один throw, а это возможно только в деструкторе объекта, лежащего на разматываемой области стека. Т.к. заранее нельзя знать, по какой причине деструктор вызван, поэтому и нельзя допускать исключений, покидающих деструктор от слова вообще. |
Автор: MyNameIsIgor 22.08.15, 11:11 |
Цитата korvin @ Я, конечно, помню, что исключения в деструкторах --- это плохо, но всякое ж бывает (возможно, кроме этого, я что-то неправильно написал в C++ коде) Просто современный компилятор юзаешь. <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> virtual ~Resource() noexcept(false) { Это ожидаемо. А это был баг в 4.7 |
Автор: Qraizer 22.08.15, 12:00 |
Да ну? Ну посмотри в "15.5.1 The std::terminate() function". Там нет ни одного упоминания исключения в деструкторе без дополнительного специального условия: Цитата Так что да, вполне ожидаемо отсутствие исключения в приведённом выше коде. А что там у вас в C++14 за баги, я не в курсе. 1 In some situations exception handling must be abandoned for less subtle error handling techniques. [ Note: These situations are: —end example ] |
Автор: D_KEY 22.08.15, 12:04 |
Цитата korvin @ Цитата D_KEY @ korvin, видимо, понимаешь не до конца. В деструкторах не должно быть исключений. Совсем. Но если там довольно сложная логика освобождения ресурса, оно, тем не менее, может произойти, из-за какой-нибудь вызываемой библиотечной функции или аппаратной ошибки, 100% гарантии нет, или как? Если есть опасения, значит отлавливай исключения в деструкторе. |
Автор: Qraizer 22.08.15, 12:11 |
Зато есть Цитата 15.2 Constructors and destructors 3 The process of calling destructors for automatic objects constructed on the path from a try block to a throw-expression is called “stack unwinding.” If a destructor called during stack unwinding exits with an exception, std::terminate is called (15.5.1). [ Note: So destructors should generally catch exceptions and not let them propagate out of the destructor. —end note ] Добавлено Ага. А ещё есть Цитата 15.4 Exception specifications так что формально деструктор без явной спецификации исключений считается nothrow. Как интересно. 15 A deallocation function (3.7.4.2) with no explicit exception-specification is treated as if it were specified with noexcept(true). |
Автор: MyNameIsIgor 22.08.15, 12:26 |
Ну да. А нахрена мне туда смотреть. Ещё рано. Начать надо с Цитата ISO/IEC 14882:2011 12.4/3 A declaration of a destructor that does not have an exception-specification is implicitly considered to have the same exception-specification as an implicit declaration (15.4). Цитата ISO/IEC 14882:2011 15.4/14 An implicitly declared special member function (Clause 12) shall have an exception-specification. If f is an implicitly declared default constructor, copy constructor, move constructor, destructor, copy assignment operator, or move assignment operator, its implicit exception-specification specifies the type-id T if and only if T is allowed by the exception-specification of a function directly invoked by f’s implicit definition; f shall allow all exceptions if any function it directly invokes allows all exceptions, and f shall allow no exceptions if every function it directly invokes allows no exceptions. А вот теперь можете смотреть и думать. Цитата Qraizer @ Так что да, вполне ожидаемо отсутствие исключения в приведённом выше коде для тех, кто не знает C++ fixed У нас всё в порядке. А вот вышеозначенные гранды опять сделали поделки, не выдерживающие элементарной критики. Добавлено Деточка, deallocation function - это не деструктор. |
Автор: Qraizer 22.08.15, 12:28 |
Мальчик, deallocation function – это общее понятие, частным случаем которого являются деструкторы. |
Автор: MyNameIsIgor 22.08.15, 12:31 |
Цитата Qraizer @ Мальчик, deallocation function – это общее понятие, частным случаем которого являются деструкторы. Нам нужен смайлик рука_кирпич... Цитату, сестра, цитату! |
Автор: Qraizer 22.08.15, 12:38 |
P.S. std::operator<<<>(std::basic_ostream<>& os, const char* s) не имеет ограничения nothrow. Добавлено Сфоткай свой экзерсис. Я с удовольствием посмотрю. Буду сильно надеяться, что последний раз. |
Автор: MyNameIsIgor 22.08.15, 12:50 |
Цитата Qraizer @ Сфоткай свой экзерсис. Я с удовольствием посмотрю. Буду сильно надеяться, что последний раз. Цитата Qraizer @ std::operator<<<>(std::basic_ostream<>& os, const char* s) не имеет ограничения nothrow. Совсем не читатель? |
Автор: D_KEY 22.08.15, 15:41 |
korvin, можешь кратенько сформулировать суть холивара? Добавлено Цитата C++ RAII/exceptions vs Golang defer/panic/recover У RAII перед defer преимущество, по сути, одно. Освобождение ресурса срабатывает всегда при разрушении объекта, причём это происходит и для полей и для базы(в случае наследования). Далее, defer выразим через RAII посредством guard'ов. RAII через defer не сделать. Что касается исключений, то это отдельный холивар. |
Автор: korvin 23.08.15, 08:39 |
Гм... RAII vs defer (как способы освобождения ресурсов) [vs другие способы освобождения ресурсов (using, with, etc), если есть кому что сказать про них] Цитата D_KEY @ У RAII перед defer преимущество, по сути, одно. Освобождение ресурса срабатывает всегда при разрушении объекта, причём это происходит и для полей и для базы(в случае наследования). Да, но такие сложные ресурсы могут запутывать и, опять же, в непредвиденном случае возникновения исключения в деструкторе одного из полей, имеем ту же проблему. Возможно, но будет ли это выражение таким же надёжным или опять его можно будет обойти и ввести систему в некорректное состояние? Да как-то и желания нет. А исключения тут как пример возможности испортить поведение механизма. |
Автор: D_KEY 23.08.15, 11:42 |
korvin, в C++ ты почти всегда можешь что-то обойти и испортить себе жизнь. Но зачем? Добавлено defer(как и with/using/etc.) можно случайно пропустить. RAII же стабилен. |
Автор: MyNameIsIgor 23.08.15, 12:29 |
Цитата korvin @ Да, но такие сложные ресурсы могут запутывать и, опять же, в непредвиденном случае возникновения исключения в деструкторе одного из полей, имеем ту же проблему. Нужно просто не выпускать исключения из деструкторов. Если следовать этому правилу, то непредвиденного не случится, ибо исключения будут ловиться и в деструкторах тех типов, значения которых мы используем в качестве полей. А вообще на практике довольно сложно получить деструктор, бросающий исключения - для этого необходимы дополнительные телодвижения. Проблема то в чём - в двух одновременно "летящих" исключениях. Что будет в Go, если в defer произойдёт паника? У тебя в коде обрабатывается только один результат recover(). |
Автор: korvin 24.08.15, 06:41 |
Мне тут, кстати, подсказали. С noexcept(false) всё работает как надо. |
Автор: D_KEY 24.08.15, 06:49 |
А сообщение Игоря об этом ты не видел? |
Автор: korvin 24.08.15, 06:55 |
Цитата MyNameIsIgor @ Проблема то в чём - в двух одновременно "летящих" исключениях. Что будет в Go, если в defer произойдёт паника? У тебя в коде обрабатывается только один результат recover(). Не читал спеку пока что, но судя по экспериментам, если уже одна паника возникла, то следующая заместит собой предыдущую, т.е. при вызове recover после двух паник, он сможет достать только объект последней паники. См. rec2. Соответственно в данном случае, чтобы поймать обе паники, нужно вызывать recover после каждой. Возможно, размер стэка паник можно настроить, но я в этом сомневаюсь, всё же механизм паник создавался не как аналог механизма исключений в других языках. Добавлено Нет. =) Уровень невнимательности 146% |
Автор: D_KEY 24.08.15, 09:21 |
Цитата korvin @ Да, но такие сложные ресурсы могут запутывать и, опять же, в непредвиденном случае возникновения исключения в деструкторе одного из полей, имеем ту же проблему. Так и деструкторы полей не должны кидать исключения И это, в принципе, нормально. Даже в книжках по яве есть рекомендации, что "функции освобождения" не должны кидать исключения. Дело не в сложности ресурса. Дело в свободе использования любых объектов. Тебе не нужно задумываться, использует ли объект ресурс. Добавлено Цитата korvin @ Возможно, но будет ли это выражение таким же надёжным или опять его можно будет обойти и ввести систему в некорректное состояние? Обойти в C++ можно почти все. ССЗБ. Случайно ты это сделать не сможешь. Добавлено Зато есть желание копипастить один и тот же defer из функции в функцию Добавлено Скорее работает так, как ты ожидаешь. Надеюсь, ты разобрался, почему? А "как надо" - это не выпускать исключение из деструктора. |
Автор: Qraizer 24.08.15, 09:36 |
Не удивительно. Пожалуй, следует применять другие методы, нежели копирования поведения. В тематике в частности, ибо он там задалбывает. |
Автор: @@@ 27.08.15, 21:38 |
Цитата D_KEY @ Даже в книжках по яве есть рекомендации, что "функции освобождения" не должны кидать исключения. Жаль, что этих книжек не читали те, кто проектировал AutoCloseable |
Автор: KILLER 28.08.15, 01:48 |
Действительно жаль |
Автор: wind 01.10.15, 00:05 |
если бы в "функциях освобождения" можно было бы обойтись без выкидывания исключений, то без них можно было обойтись вообще |
Автор: D_KEY 01.10.15, 05:32 |
Цитата wind @ если бы в "функциях освобождения" можно было бы обойтись без выкидывания исключений, то без них можно было обойтись вообще Без исключений? Во-первых, без них действительно можно обойтись. Во-вторых, если "функциям освобождения" не кидают исключения, то это существенно упрощает построение безопасного в плане исключений кода. В-третьих, каким образом из вашей посылки следует ваш вывод? В С++, например, деструкторы исключений не кидают, при этом исключения могут активно использоваться. |
Автор: Qraizer 01.10.15, 12:45 |
Цитата D_KEY @ Не упрощают, а делает возможным. И "в плане исключений" лишнее. Любые состояния ошибок в любых функциях очистки делают код абсолютно ничего не гарантирующим. Во-вторых, если "функциям освобождения" не кидают исключения, то это существенно упрощает построение безопасного в плане исключений кода. |
Автор: D_KEY 03.10.15, 09:25 |
В C++. В Java исключение в "функции освобождения" ничего не сломает и не приведет к потери информации. Цитата Любые состояния ошибок в любых функциях очистки делают код абсолютно ничего не гарантирующим. Тем не менее, в функциях очистки ошибки могут происходить. Например потому, что API ОС, как правило, предполагает коды ошибок для функций закрытия. В C++ в деструкторе ты можешь(после логирования) либо проигнорировать либо упасть. Добавлено Цитата D_KEY @ В Java исключение в "функции освобождения" ничего не сломает и не приведет к потери информации Поясню: <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> import java.lang.*; class A implements AutoCloseable { public void close() { throw new RuntimeException("CloseError"); } } class Ideone { public static void main (String[] args) throws java.lang.Exception { try(A a = new A()) { throw new RuntimeException("MainError"); } } } <{CODE_COLLAPSE_OFF}><{CODE_WRAP_OFF}> Exception in thread "main" java.lang.RuntimeException: MainError at Ideone.main(Main.java:22) Suppressed: java.lang.RuntimeException: CloseError at A.close(Main.java:11) at Ideone.main(Main.java:23) http://ideone.com/WRjcGH#stderr Естественно, мы сможем сами так же отловить и пройтись по списку подавленных исключений. |
Автор: Qraizer 03.10.15, 14:32 |
Цитата D_KEY @ Естественно. Но любая такая хрень означает, что ошибка в программе. Например, почему CloseHandle() может упасть? Разве что из-за испорченнего HANDLE, который потёр кривой поинтер. Или потому, что тот уже закрыт. Когда все подобные ошибки будут исправлены, функции очистки тоже будут успешны.Тем не менее, в функциях очистки ошибки могут происходить. Например потому, что API ОС, как правило, предполагает коды ошибок для функций закрытия. Речь о том, что в безошибочной программе функции очистки никогда не столкнутся с такими ситуациями. Поэтому разрабатывать их с учётом подобных ситуаций нет необходимости. Если же такая необходимость возникла, то это значит лишь то, что программа плохо спроектирована, и некоторые действия в ней не должны размещаться в функциях очистки, т.к. не связаны с освобождением ресурсов. Помнишь, например, дискуссию о FreeAndNIL() в контексте Clear(), вызываемой в деструкторе? Там сами дельфисты нагородили костылей и пытались показать, что это нормально. |
Автор: korvin 03.10.15, 18:13 |
Проблема в том, что языки, в основном, предоставляют возможность "подавления" таких ошибок, т.к. ошибка может быть в сторонней либе, которую приходится использовать независимо от её кривости (например, если воркэроунд быстрее, проще и дешевле, чем повторная реализация нужного функционала без косяков). В итоге получаем, что ассерты и паники можно таки отловить, нивелируя их цель --- гарантированное падение программы и сигнализация о баге. |
Автор: amk 04.10.15, 09:43 |
Вообще-то закрытию могут помешать и внешние причины. К примеру, программа открывает файл, скажем на флэшке, и время от времени что-то туда пишет. Пользователь знает, при каких условиях происходит запись, и зная, что больше ничего писаться не будет, выдёргивает флэшку. Это неправильно, но есть такие, что не выполняют отключения устройства. Или флэшка может быть настроена на быстрое извлечение. При попытке закрыть файл произойдёт ошибка. И часть информации при этом потеряется. В качестве другого примера можно привести работу с каким-нибудь оборудованием. Часто такое оборудование можно отключить вручную. Правда в таких случаях ошибка не так серьёзна. Цитата korvin @ Подавление ошибки в таком случае - это костыль, позволяющий пользоваться кривой библиотекой. Но такое подавление само может обеспечить тебе отсутствие гарантий правильной работы программы. Если программист таким способом "борется" с ошибками в программе. За травму, полученную в результате нарушения работником ТБ работодатель ответственности не несёт. И за баги программы возникшие в результате маскировки ошибок библиотеки, ответственность несёт уже не разработчик кривой библиотеки, а тот программист который эти ошибки таким образом спрятал.в основном, предоставляют возможность "подавления" таких ошибок, т.к. ошибка может быть в сторонней либе, которую приходится использовать независимо от её кривости И вообще, эта библиотека, возможно, не предназначалась для такого использования. |