c++exceptionpolymorphismdynamic-castnested-exceptions

std::nested_exception and polymorphism - Is this the best that can be done?


std::nested_exceptions are nice when all you want to do is calling what(), but accessing the interface of other exception types gets ugly.

Let us suppose I have two exception classes which store some additional information:

/* CODE BLOCK 1 */
class ErrorI : public std::runtime_error {
public:
    ErrorI(int a_integer) : std::runtime_error{"ErrorI"}, integer{a_integer} {}
    int integer;
};

class ErrorD : public std::runtime_error {
public:
    ErrorD(double a_real) : std::runtime_error{"ErrorD"}, real{a_real} {}
    double real;
};

Without nested exceptions, we can access the member variables in the try/catch block:

/* CODE BLOCK 2 */
int main()
{
    try {
        /* do stuff */;
    }
    catch(const ErrorI& ee){
        std::cout << "  Value: " << ee.integer << std::endl;
    }
    catch(const ErrorD& ee){
        std::cout << "  Value: " << ee.real << std::endl;
    }
}

But if we want to unwrap a std::nested_exception, things are not so straightforward. We need to define a function which will be called recursively, which should look like this:

/* CODE BLOCK 3 */
void process_exception(const std::exception& e, int level=0) {
    try {
        std::rethrow_if_nested(e);
    }
    catch(const std::exception& e) {
        process_exception(e, level+1);
    }

    /* ... process the top-most (latest) exception ... */
}

Unfortunately, for processing the top-most exception, we cannot use the try/catch syntax in Code Block 2: if we rethrow e, it will be truncated to an std::exception, and we will lose all the additional information. EDIT: This is not true if std::rethrow_exception and std::exception_ptr are used.

So we're back to the problem of good-ole dynamic type-checking, with all that it entails (see this for example).

  1. Derive all the exceptions from a common base class with the desired interface. This includes approaches like the Visitor pattern. This is neat, but not good if the exception classes are provided by a external library.

  2. Using dynamic_cast:

     /* CODE BLOCK 4 */
     if (auto p = dynamic_cast<ErrorI const*>(&e)) {
         std::cout << "  Value: " << p->integer << std::endl;
     }
     else if (auto p = dynamic_cast<ErrorD const*>(&e)) {
         std::cout << "  Value: " << p->real << std::endl;
     }
    
  3. ???

My only option seems to resort to 2. I would love to hear if there is any other suggestion.


Solution

  • With the help of std::exception_ptr, you might do something like:

    void print_exception(const std::exception_ptr& eptr, int level =  0)
    {
        try
        {
            std::rethrow_exception(eptr);
        }
        catch (const ErrorI& e)
        {
            std::cerr << std::string(level, ' ') << "exception: " << e.what() << ": " << e.integer << std::endl;
        }
        catch (const ErrorD& e)
        {
            std::cerr << std::string(level, ' ') << "exception: " << e.what() << ": " << e.real << std::endl;
        }
        catch (const std::exception& e)
        {
            std::cerr << std::string(level, ' ') << "exception: " << e.what() << std::endl;
        }
    }
    
    // prints the explanatory string of an exception. If the exception is nested,
    // recurses to print the explanatory of the exception it holds
    void print_exception_rec(const std::exception_ptr& eptr, int level =  0)
    {
        print_exception(eptr, level);
        try {
            try {
                std::rethrow_exception(eptr);
            }
            catch (const std::exception& e)
            {
                std::rethrow_if_nested(e);
            }
        } catch(const std::exception&) {
            print_exception_rec(std::current_exception(), level+1);
        } catch(...) {}
    }
    

    Demo