We shouldn't be allowing exceptions to propagate from a destructor. Doing that is always bad in C++. Any code which does it should be treated as a bug and fixed.
It's okay to have exceptions thrown inside a destructor, just as long as you catch() them inside the destructor too and don't allow them to propagate out. On Fri, Jun 6, 2014 at 8:52 AM, chris beck <beck...@gmail.com> wrote: > Dear List, > > A few days ago I was looking at some of the code in the engine on the > master branch, and I realized that we currently are doing > some questionable things with exceptions and destructors that can cause > the program to crash. > > > https://github.com/wesnoth/wesnoth/blob/46299c6a110b09ebfa9f9e3044021ece2568ea4d/src/playmp_controller.cpp#L426 > > At this line, we signal a "quit" from a client event loop state > `end_level_exception (QUIT)`. This is not uncommon in the engine, > we currently use "end_level_exception" and "end_turn_exception" to signal > when a turn has ended or a level had ended, and in mp > we sometimes throw "network::error" when we want to disconnect from the > current game. > > It's debatable whehter using exceptions for flow control is good or bad, > and I guess many people have opinions about it. > However, in this case, it actually leads to crash, because we also have an > object on the stack `turn_info_send` which sends > networking data in it's destructor: > > > https://github.com/wesnoth/wesnoth/blob/afbf5c6e5804ef5a31a40b4bcb59a41f389e5a40/src/playturn.hpp#L91 > > What this means is that if the networking module has an error, like, it > loses connection with the server, instead of handling > that network::error exception normally, and jumping here: > > > https://github.com/wesnoth/wesnoth/blob/afbf5c6e5804ef5a31a40b4bcb59a41f389e5a40/src/playsingle_controller.cpp#L589 > > which would tell the user "a network error has occurred, would you like to > save the game?", the program will *immediately crash > to desktop*. (Because, as I understand, in C++, if an exception is thrown, > the callstack is unwound until a handler for the > exception is reached, or the program is terminated, and in this process > all destructors are called for any objects on the stack. > If any of those destructors throws an exception, the program is > immediately terminated -- you cannot handle multiple exceptions > at once.) > > I'm not sure because there is a lot of code involved in synchroniation and > networking, but I'm concerned that issues like this > from somewhere in the code could be causing instability on 1.12 branch. > For instance we got some bug reports like this: > > https://gna.org/bugs/?21873 > > "We played a couple multiplayer matches, each one ended with him losing > but everytime he lost the game reported him as having disconnected and when > he got back he said that the game had forcibly shut down. This would happen > in the middle of the post-game screen even before I hit end scenario." > > That's pretty much exactly what I would expect if we were getting some > kind of double exception issues. > > --- > > There are two morals that I take away from this: > > 1.) Don't use exceptions for flow control. > 2.) Don't throw exceptions from the destructor. > > As I understand there are some C++ developers out there who will argue on > behalf of both of these things, but if used in concert they will apparently > lead to very poor results. It is sometimes said, it takes two aggressive > drivers to make an accident, but at this point I think we have had an > accident and now should add remarks to "coding standards" on the wiki > disallowing both of these. > > --- > > A bit more on each -- > > (2) After reading about this (I learned in school that you shouldn't do > this but I long ago forgot why and had to read about it...), it sounds like > there are some C++ developers that actually defend this practice of > throwing from a destructor in some cases. In C++98 if you can somehow > guarantee that there is no exception actively being unwound, then throwing > from the destructor won't immediately crash the program. However: > - It will prevent destructors of children being called and can therefore > lead to memory leaks that are very difficult if not impossible to correct > after the throw, > - In C++11 the language is more strict about this -- if a destructor ever > throws an exception, the program is immediately terminated. > So it sounds like it's not something that we want to do. In fact, it might > be a good idea if we tell people that if they do anything nontrivial in a > destructor, they must wrap it in a try { stuff } catch (...) {} block that > will swallow all exceptions. Even writing to a logger in a destructor could > in some cases lead to a crash I think: > > a.) Suppose that at some time in the future, there is a bug our > UTF-conversion facilities. A bad string conversion happens while someone is > writing to a logger object -- the logger chokes and throws some kind of > exception which propogates up the play_controller stack. > > If someone somewhere wrote a destructor X::~X() { logger << "foo bar\n" ; > }, if the logger is already broken it could generate a second exception, > immediately crashing the program rather than reporting the error. > > b.) gfgtdf and I have written a "smart enum" macro which is supposed to > help us when parsing WML files to C++ objects, when there are only a few > valid options for strings. The enums have streaming operations defined so > that they can be converted to strings and back, and these ops have built-in > error reporting -- if an enum can't be parsed or written, and the game is > running in debug mode, it will generate a t_wml_exception, which launches a > transient gui window explaining to the user that they have an error in > their WML. There are several places high up in play_controller where these > exceptions are hanlded and the gui window is actually launched. > > However, suppose someone wrote a destructor that logged some debugging > output based on converting some more enums, or similarly calls lexical cast > in some context. In that case if we encounter a bad enum value and throw an > exception, then when X is destroyed we might encounter it _a second time_, > throw a _second_ exception while writing the debugging, and crash instantly > because of it. > > So I think it should be a coding standards issue that destructors that do > more than, delete pointers, or call a C library "release resource" > function, should be wrapped in a try-catch-all block. If the programmer > doesn't like this because it would interfere with normal exception > handling, then they should consider not putting their code into a > destructor. > > However I'm not a C++ expert by any means, and I normally try to stay away > from destructors anyways, so there are likely more things I don't know > about all of this -- please chime in if you have something to add. > > > (1) I guess that this is somewhat controversial, and there are some C++ > developers who think that using exceptions for flow control is fine esp. if > it leads to less typing. > > However, my viewpoint is that, the reason we were tempted to start doing > (2) to handle networking ops in the engine, is because we were doing (1) > and had no way to guarantee that we are staying in sync unless networking > was happening in destructors. So I think (1) is in some way the root cause > of this. > > I have started a branch here, which attempts to incrementally push all of > the flow control exceptions out of the play_controller object at least, and > down further in the call stack into replay execution and event handling. > The goal of this is to simplify the logic that is happening in play > controller, so that we can properly reason about it and ensure networking > synchronization is happening correctly without using exceptions or > destructors. > > https://github.com/wesnoth/wesnoth/pull/191 > > I view this branch as having been fairly productive, just the review > process has led me to find some other bugs elsewhere in 1.12 branch that > could be corrected, and it also clarifies exactly where the end level and > end turn signals are coming from so that we can better understand exactly > what is going on. > > The experience has led me to think that, we should add a line to the > coding standards page: > http://wiki.wesnoth.org/CodingStandards#Write_exception-safe_code > > "Do not use exceptions for normal flow control. This is generally > unnecessary, leads to less maintainable code, and can interfere with the > proper handling of truly exceptional scenarios." > > The purpose of the coding standards remarks is not to criticize other > programmers, but rather to make sure that new programmers actually know > about these things and that they can cause crashes -- the rules about > exceptions and destructors are fairly unique to C++ anyways and not > everyone knows them intimately... > > Best Regards, > Chris Beck > > _______________________________________________ > Wesnoth-dev mailing list > Wesnoth-dev@gna.org > https://mail.gna.org/listinfo/wesnoth-dev > >
_______________________________________________ Wesnoth-dev mailing list Wesnoth-dev@gna.org https://mail.gna.org/listinfo/wesnoth-dev