views:

978

answers:

7

There are lots of places where guidelines for designing exception classes can be found. Almost everywhere I look, there's this list of things exception objects should never do, which impacts the design of those classes.

For instance, the Boost people recommend that the class contain no std::string members, because their constructor could throw, which would cause the run-time to terminate the program immediately.

Now, it seems to me that this is rather theoretical. If std::string's constructor throws, it's either a bug (I passed a null-pointer in) or an out-of-memory condition (correct me if I'm wrong here). Since I'm on a desktop, I just pretend I have an infinite amount of memory, and running out of memory is fatal to my application no matter what.

With that in mind, why shouldn't I embed std::string objects in my exception classes? In fact, why couldn't my exception classes be full-featured, and also take care of logging, stack tracing, etc. I'm aware of the one-responsibility principle, and it seems to me to be a fair trade-off to have the exception class do all that. Surely, if my parser needs to report a syntax error, an full-featured exception would be more helpful than an exception built around a statically allocated character array.

So: lean C++ exception classes - how big a deal is it in the real-world? What are the trade-offs? Are there good discussions on the topic?

+2  A: 

As a general case, exception classes should be simple, self-sufficient structures and never allocate memory (like std::string does). The first reason is that allocations or other complex operations may fail or have side effects. The other reason is that exception objects are passed by value and thus are stack-allocated, so they must be as light-weighted as possible. Higher level features should be handled by the client code, not the exception class itself (unless for debugging purpose).

fbonnet
Well, that's exactly my question: if I assume I have all the memory in the world, and that performance does not matter in exception handling, why do I need to take those precautions? Why would my parse-failed exception not be allowed to allocate, if it makes error handling easier?
Carl Seleborg
Even with unlimited memory instant CPU operations, exceptions still must avoid side effects because by definition they modify the state of the app and may in turn throw exceptions themselves. The more complex your class is, the more likely are side effects. F.ex. avoid logging from your exception.
fbonnet
Why do you assume you have all the memory in the world? What happens when your app crashes because of a memory leak? How are you going to ever find that leak, if you willfully throw away all hope of handling the exception?
jalf
+3  A: 

You could use the Boost.Exception library to help define your exception hierarchy. The Boost.Exception library supports the:

transporting of arbitrary data to the catch site, which is otherwise tricky due to the no-throw requirements (15.5.1) for exception types.

The limitations of the framework will provide you with reasonably defined design parameters.

Boost.Exception
See also: Boost.System

Functastic
+2  A: 

The number one job of an exception, well before any consideration to allowing code to handle the exception, is to be able to report to the user and/or dev exactly what went wrong. An exception class that cannot report OOM but just crashes the program without providing any clue to why it crashed is not worth much. OOM is getting pretty common these days, 32-bit virtual memory is running out of gas.

The trouble with adding a lot of helper methods to an exception class is that it will force you into a class hierarchy that you don't necessarily want or need. Deriving from std::exception is now required so you can do something with std::bad_alloc. You'll run into trouble when you use a library that has exception classes that don't derive from std::exception.

Hans Passant
+2  A: 

Have a look at the std exceptions they all use std::string internally.
(Or should I say my g++ implementation does, I am sure the standard is silent on the issue)

/** Runtime errors represent problems outside the scope of a program;
  *  they cannot be easily predicted and can generally only be caught as
  *  the program executes.
  *  @brief One of two subclasses of exception.
 */
class runtime_error : public exception
{
    string _M_msg;
  public:
    /** Takes a character string describing the error.  */
    explicit runtime_error(const string&  __arg);

    virtual ~runtime_error() throw();

    /** Returns a C-style character string describing the general cause of
     *  the current error (the same string passed to the ctor).  */
    virtual const char* what() const throw();
};

I usually derive my exceptions from runtime_error (or one of the other standard exceptions).

Martin York
A: 

I think refusing to use std::string in exception classes is unnecessary purism. Yes, it can throw. So what? If your implementation of std::string throws for reasons other than running out of memory just because you're constructing a message "Unable to parse file Foo", then there is something wrong with the implementation, not with your code.

As for running out of memory, you have this problem even when you construct an exception which takes no string arguments. Adding 20 bytes of helpful error message is unlikely to make or break things. In a desktop app, most OOM errors happen when you try to allocate 20 GB of memory by mistake, not because you've been happily running at 99.9999% capacity and something tipped you over the top.

quant_dev
There is something "wrong with the implementation" if you run out of memory when allocating a string? How is the implementation at fault for *your* memory leaks?
jalf
I have clarified my response so that other readers do not misunderstand it as you did.
quant_dev
A: 

Since I'm on a desktop, I just pretend I have an infinite amount of memory, and running out of memory is fatal to my application no matter what.

So when your app fatally fails, wouldn't you prefer it to terminate cleanly? Let destructors run, file buffers or logs to be flushed, maybe even display an error message (or even better, a bug reporting screen) to the user?

With that in mind, why shouldn't I embed std::string objects in my exception classes? In fact, why couldn't my exception classes be full-featured, and also take care of logging, stack tracing, etc. I'm aware of the one-responsibility principle, and it seems to me to be a fair trade-off to have the exception class do all that.

Why is that a fair trade-off? Why is it a trade-off at all? A trade-off implies that you make some concessions to the Single-Responsibility Principle , but as far as I can see, you don't do that. You simply say "my exception should do everything". That's hardly a trade-off.

As always with the SRP, the answer should be obvious: What do you gain by making the exception class do everything? Why can't the logger be a separate class? Why does it have to be performed by the exception? Shouldn't it be handled by the exception handler? You may also want to localization, and provide syntax error messages in different languages. So your exception class should, while being constructed, go out and read external resource files, looking for the correct localized strings? Which of course means another potential source of errors (if the string can't be found), adds more complexity to the exception, and requires the exception to know otherwise irrelevant information (which language and locale settings the user uses). And the formatted error message may depend on how it is being shown. Perhaps it should be formatted differently when logged, when shown in a message box, or when printed to stdout. More problems for the exception class to deal with. And more things that can go wrong, more code where errors can occur.

The more your exception tries to do, the more things can go wrong. If it tries to log, then what happens if you run out of disk space? Perhaps you also assume infinite disk space, and just ignore that if and when it happens, you'll be throwing away all the error information? What if you don't have write permission to the log file?

From experience, I have to say that there are few things more annoying than not getting any information about the error that just occurred, because an error occurred. If your error handling can't handle that errors occur, it isn't really error handling. If you exception class can't handle being created and thrown without causing more exceptions, what is the point?

Normally, the reason for the SRP is that the more complexity you add to a class, the harder it is to ensure correctness, and to understand the code. That still applies to exception classes, but you also get a second concern: The more complexity you add to the exception class, the more opportunities there are for errors to occur. And generally, you don't want errors to occur while throwing an exception. You're already in the middle of handling another error, after all.

However, the rule that "an exception class should not contain a std::string isn't quite the same as "an exception class is not allowed to allocate memory". std::exception does the latter. It stores a C-style string after all. Boost just says not to store objects which may throw exceptions. So if you allocate memory, you just have to be able to handle the case where allocation fails.

Surely, if my parser needs to report a syntax error, an full-featured exception would be more helpful than an exception built around a statically allocated character array.

Says the person who just said he didn't mind the app just terminating with no feedback to the user if an error occurred. ;)

Yes, your exception should contain all the data you need to produce a friendly, readable error message. In the case of a parser, I'd say that would have to be something like:

  • Input file name (or a handle or pointer which allows us to fetch the filename when needed)
  • The line number at which the error occurred
  • Perhaps the position on the line
  • The type of syntax error that occurred.

Based on this information, you can when handling the error produce a nice friendly, robust error message for the user. You can even localize it if you like. You can localize it when you handle the exception.

Generally, exception classes are for use by the programmer. They should not contain or construct text aimed at the user. That can be complex to create correctly, and should be done when handling the error.

jalf
Jalf, you misread what I wrote: I said that in case of exhausted memory, there was not much more I could/was willing to do (certainly not pop up a window). In any other case, I would want my exception to include as much info as possible, which means allocating memory (which in turn means potentially throwing an exception).The trade-off is between having one class that gets harder to maintain, and thousands of call sites where I want one single line of code to collect lots of data about the error.
Carl Seleborg
No, I think you misread my point. Yes, the exception should hold enough data to describe the problem that occurred. But it doesn't have to hold a user-friendly message describing it. It just has to hold the data necessary to construct such a message, and that can *often* (not *always*) be done without dynamic allocations.In the case of exhausted memory, there is something more you can do. You can log the exception allowing you, the developer to deal with it later. But the exception won't get logged if simply constructing the exception failed because it tried to allocate memory.
jalf
+1  A: 

The C++ standard requires that exceptions have no-throw copy costructor. If you have a std::string member, you don't have a no-throw copy constructor. If the system fails to copy your exception, it'll terminate your program.

It is also a good idea to use virtual inheritance in designing your exception type hierarchy, as explained in http://www.boost.org/doc/libs/release/libs/exception/doc/using_virtual_inheritance_in_exception_types.html.

However, there is no requirement that exception objects be simple or not allocate memory. In fact, exception objects themselves are typically allocated on the heap, so the system may run out of memory in an attempt to throw an exception.

Emil