C++ exception class design

34,064

Solution 1

Use a shallow hierarchy of exception classes. Making the hierarchy too deep adds more complexity than value.

Derive your exception classes from std::exception (or one of the other standard exceptions like std::runtime_error). This allows generic exception handlers at the top level to deal with any exceptions you don't. For example, there might be an exception handler that logs errors.

If this is for a particular library or module, you might want a base specific to your module (still derived from one of the standard exception classes). Callers might decide to catch anything from your module this way.

I wouldn't make too many exception classes. You can pack a lot of detail about the exception into the class, so you don't necessarily need to make a unique exception class for each kind of error. On the other hand, you do want unique classes for errors you expect to handle. If you're making a parser, you might have a single syntax_error exception with members that describe the details of the problem rather than a bunch of specialty ones for different types of syntax errors.

The strings in the exceptions are there for debugging. You shouldn't use them in the user interface. You want to keep UI and logic as separate as possible, to enable things like translation to other languages.

Your exception classes can have extra fields with details about the problem. For example, a syntax_error exception could have the source file name, line number, etc. As much as possible, stick to basic types for these fields to reduce the chance of constructing or copying the exception to trigger another exception. For example, if you have to store a file name in the exception, you might want a plain character array of fixed length, rather than a std::string. Typical implementations of std::exception dynamically allocate the reason string using malloc. If the malloc fails, they will sacrifice the reason string rather than throw a nested exception or crashing.

Exceptions in C++ should be for "exceptional" conditions. So the parsing examples might not be good ones. A syntax error encountered while parsing a file might not be special enough to warrant being handled by exceptions. I'd say something is exceptional if the program probably cannot continue unless the condition is explicitly handled. Thus, most memory allocation failures are exceptional, but bad input from a user probably isn't.

Solution 2

Use virtual inheritance. This insight is due to Andrew Koenig. Using virtual inheritance from your exception's base class(es) prevents ambiguity problems at the catch-site in case someone throws an exception derived from multiple bases which have a base class in common.

Other equally useful advice on the boost site

Solution 3


2: No you should not mix user interface (=localized messages) with program logic. Communication to the user should be done at an outer level when the application realises that it cannot handle the issue. Most of the information in an exception is too much of an implementation detail to show a user anyway.
3: Use boost.exception for this
5: No dont do this. See 2. The decision to log should always be at the error handling site.

Dont use only one type of exception. Use enough types so the application can use a separate catch handler for each type of error recovery needed

Solution 4

Not directly related to the design of an exception class hierarchy, but important (and related to using those exceptions) is that you should generally throw by value and catch by reference.

This avoids problems related to managing the memory of the thrown exception (if you threw pointers) and with the potential for object slicing (if you catch exceptions by value).

Solution 5

Since std::nested_exception and std::throw_with_nested have become available with C++11, I would like to point to answers on StackOverflow here and here

Those answers describe how you can get a backtrace on your exceptions inside your code without need for a debugger or cumbersome logging, by simply writing a proper exception handler which will rethrow nested exceptions.

The exception design there, in my opinion, also suggests to not create exception class hierarchies, but to only create a single exception class per library (as already pointed out in an answer to this question).

Share:
34,064
Carmen
Author by

Carmen

Updated on July 09, 2022

Comments

  • Carmen
    Carmen almost 2 years

    What is a good design for a set of exception classes?

    I see all sorts of stuff around about what exception classes should and shouldn't do, but not a simple design which is easy to use and extend that does those things.

    1. The exception classes shouldn't throw exceptions, since this could lead straight to the termination of the process without any chance to log the error, etc.
    2. It needs to be possible to get a user friendly string, preferable localised to their language, so that there's something to tell them before the application terminates itself if it can't recover from an error.
    3. It needs to be possible to add information as the stack unwinds, for example, if an XML parser fails to parse an input stream, to be able to add that the source was from a file, or over the network, etc.
    4. Exception handlers need easy access to the information they need to handle the exception.
    5. Write formatted exception information to a log file (in English, so no translations here).

    Getting 1 and 4 to work together is the biggest issue I'm having, since any formatting and file output methods could potentially fail.

    EDIT: So having looked at exception classes in several classes, and also in the question Neil linked to, it seems to be common practice to just completely ignore item 1 (and thus the boost recommendations), which seems to be a rather bad idea to me.

    Anyway, I thought I'd also post the exception class I'm thinking of using.

    class Exception : public std::exception
    {
        public:
            // Enum for each exception type, which can also be used
            // to determine the exception class, useful for logging
            // or other localisation methods for generating a
            // message of some sort.
            enum ExceptionType
            {
                // Shouldn't ever be thrown
                UNKNOWN_EXCEPTION = 0,
    
                // The same as above, but it has a string that
                // may provide some information
                UNKNOWN_EXCEPTION_STR,
    
                // For example, file not found
                FILE_OPEN_ERROR,
    
                // Lexical cast type error
                TYPE_PARSE_ERROR,
    
                // NOTE: in many cases functions only check and
                //       throw this in debug
                INVALID_ARG,
    
                // An error occured while trying to parse
                // data from a file
                FILE_PARSE_ERROR,
            }
    
            virtual ExceptionType getExceptionType()const throw()
            {
                return UNKNOWN_EXCEPTION;
            }
    
            virtual const char* what()throw(){return "UNKNOWN_EXCEPTION";}
    };
    
    
    class FileOpenError : public Exception
    {
        public:
            enum Reason
            {
                FILE_NOT_FOUND,
                LOCKED,
                DOES_NOT_EXIST,
                ACCESS_DENIED
            };
            FileOpenError(Reason reason, const char *file, const char *dir)throw();
            Reason getReason()const throw();
            const char* getFile()const throw();
            const char* getDir ()const throw();
    
        private:
            Reason reason;
            static const unsigned FILE_LEN = 256;
            static const unsigned DIR_LEN  = 256;
            char file[FILE_LEN], dir[DIR_LEN];
    };
    

    Point 1 is addressed since all strings are handled by copying to an internal, fixed size buffer (truncating if needed, but always null terminated).

    Although that doesn't address point 3, however I think that point is most likely of limited use in the real world anyway, and could most likely be addressed by throwing a new exception if needed.

  • Pavel Minaev
    Pavel Minaev over 14 years
    How about not deriving exceptions from multiple bases in the first place? I don't mind MI in general, but I don't see any reason whatsoever to use it for exception classes.
  • jon-hanson
    jon-hanson over 14 years
    Is it inconceivable that an exception might be caused by more than one type of problem?
  • Carmen
    Carmen over 14 years
    "The strings in the exceptions are there for debugging. You shouldn't use them in the user interface. You want to keep UI and logic as separate as possible, to enable things like translation to other languages." so where and how should such strings be generated, as you said exceptiosn are useaully stuff that cause the program to exit, or at least fail to do what the user wanted to do, so there needs to be a textural representation that is end user friendly for the vast majority of exceptions.
  • Adrian McCarthy
    Adrian McCarthy over 14 years
    The catch block that handles the exception should load the UI string from resources. Something like: catch (const syntax_error &ex) { std::cerr << ex.filename() << "(" << ex.line_number() << "): " << GetText(IDS_SYNTAXERROR) << std::endl; }, where GetText() loads a string from your program's resources.
  • pgast
    pgast over 14 years
    Someone obviously writes applications and not libraries. The structure is legal C++ and as such it will be used regardless of the fact that one does not "see any reason whatsoever to use it for exception classes". You owe Jon an upvote if not an apology – pgast 0 secs ago
  • Gizmo
    Gizmo almost 7 years
    would C++11 or C++17 change anything in your answer? I'm more interested in C++11 than C++17 though.
  • Adrian McCarthy
    Adrian McCarthy almost 7 years
    @Gizmo: I'm still learning about what's in C++17, but nothing in C++11 would prompt me to change anything I said here. There might be some use for constexpr or move-constructors and move-assignment in an exception class in order to avoid hitting additional problems while propagating an exception, but if you actually needed those, I'd wonder whether your exception class is too complicated.