9

I'm coding a small library and I'm having some trouble with designing the exception handling. I must say that I am (still) confused by this feature of the C++ language and I tried to read as much as possible on the subject to come to an understanding of what would I have to do to properly work with exception classes.

I decided to use a system_error type of approach taking inspiration from the STL implementation of the future_error class.

I have an enumeration containing the error codes:

enum class my_errc : int
{
    error_x = 100,
    error_z = 101,
    error_y = 102
};

and a single exception class (backed up by an error_category type of structures and everything else needed by the system_error model):

// error category implementation
class my_error_category_impl : public std::error_category
{
    const char* name () const noexcept override
    {
        return "my_lib";
    }

    std::string  message (int ec) const override
    {
        std::string msg;
        switch (my_errc(ec))
        {
        case my_errc::error_x:
            msg = "Failed 1.";
            break;
        case my_errc::error_z:
            msg = "Failed 2.";
            break;
        case my_errc::error_y:
            msg = "Failed 3.";
            break;
        default:
            msg = "unknown.";
        }

        return msg;
    }

    std::error_condition default_error_condition (int ec) const noexcept override
    {
        return std::error_condition(ec, *this);
    }
};

// unique instance of the error category
struct my_category
{
    static const std::error_category& instance () noexcept
    {
        static my_error_category_impl category;
        return category;
    }
};

// overload for error code creation
inline std::error_code make_error_code (my_errc ec) noexcept
{
    return std::error_code(static_cast<int>(ec), my_category::instance());
}

// overload for error condition creation
inline std::error_condition make_error_condition (my_errc ec) noexcept
{
    return std::error_condition(static_cast<int>(ec), my_category::instance());
}

/**
 * Exception type thrown by the lib.
 */
class my_error : public virtual std::runtime_error
{
public:
    explicit my_error (my_errc ec) noexcept :
        std::runtime_error("my_namespace ")
        , internal_code(make_error_code(ec))
    { }

    const char* what () const noexcept override
    {
        return internal_code.message().c_str();
    }

    std::error_code code () const noexcept
    {
        return internal_code;
    }

private:
    std::error_code internal_code;
};

// specialization for error code enumerations
// must be done in the std namespace

    namespace std
    {
    template <>
    struct is_error_code_enum<my_errc> : public true_type { };
    }

I only have a small number of situations in which I throw exceptions illustrated by the error code enumeration.

The above did not sit well with one of my reviewers. He was of the opinion that I should have created a hierarchy of exception classes with a base class derived from std::runtime_error because having the error code embedded in the condition mixes things - exceptions and error codes - and it would be more tedious to deal with a the point of handling; the exception hierarchy would also allow for easy customization of the error message.

One of my arguments was that I wanted to keep it simple, that my library did not need to throw multiple types of exceptions and that the customization is also easy in this case as it is handled automatically - the error_code has an error_category associated with it that translates the code to the proper error message.

I have to say that I did not defend well my choice, testament to the fact that I still have some misunderstandings regarding C++ exceptions.

I would like to know if my design makes sense. What would be the advantages of the other method over the one I chose as I have to admit I fail to see that also? What could I do to improve?

1
  • 2
    I tend to agree in principle with your reviewer (mixing error codes and exceptions is not really that useful). But unless you have a huge library having a big hierarchy is also not useful. A base exception that contains a message string, only have separate exceptions if the catcher of the exception can potentially use the uniqueness of the exception to fix the problem. Apr 15, 2015 at 7:32

2 Answers 2

9

I think your colleague was right: you are designing your exception cases based on how simple it is to implement within the hierarchy, not based on the exception-handling needs of the client code.

With one exception type and an enumeration for the error condition (your solution), if the client code needs to handle single error cases (for example, my_errc::error_x) they must write code like this:

try {
    your_library.exception_thowing_function();
} catch(const my_error& err) {
    switch(err.code()) { // this could also be an if
    case my_errc::error_x:
        // handle error here
        break;
    default:
        throw; // we are not interested in other errors
    }
}

With multiple exception types (having a common base for the entire hierarchy), you can write:

try {
    your_library.exception_thowing_function();
} catch(const my_error_x& err) {
    // handle error here
}

where exception classes look like this:

// base class for all exceptions in your library
class my_error: public std::runtime_error { ... };

// error x: corresponding to my_errc::error_x condition in your code
class my_error_x: public my_error { ... };

When writing a library, the focus should be on it's ease of use, not (necessarily) ease of the internal implementation.

You should only compromize the ease of use (how client code will look like) when the effort of doing it right in the library, is prohibitive.

0

I agree with your reviewers and with @utnapistim. You can use system_error approach when you implement cross-platform things when some errors require special handling. But even in this case, it is not a good solution, but less evil solution.

One more thing. When create exception hierarchy, do not make it very deep. Create only those exception classes, that can be processed by clients. In most cases I use only std::runtime_error and std::logic_error. I throw std::runtime_error when something goes wrong and and I can do nothing (user eject device from computer, he forgot that application still running), and std::logic_error when program logic broken (User tries to delete record from database that does not exist, but before delete operation he can check it, so he get logic error).

And as library developer, think about your users needs. Try to use it yourself and think, whether it comfort for you. Than you can explain your position to your reviewers with examples of code.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.