Search code examples
c++templatesexceptionmultiple-inheritancevirtual-inheritance

Multiple inheritance of a templated exception class in C++


Why does:

#include <iostream>

struct base_exc : std::runtime_error
{
  base_exc(const std::string& s): std::runtime_error(("base_exc: " + s).c_str()){}
};

struct derived_exc1 : base_exc
{
  derived_exc1(const std::string& s): base_exc(("derived_exc1: " + s).c_str()){}
};

struct derived_exc2 : base_exc
{
  derived_exc2(const std::string& s): base_exc(("derived_exc2: " + s).c_str()){}
};

template <typename T1, typename T2>
struct binary_exc: T1, T2
{
  binary_exc(const std::string& s): T1(s), T2(s){}
};

int main()
{
  try{
    throw binary_exc<derived_exc2, derived_exc1>("something occured");
  }
  catch(base_exc const& e)
  {
    std::cout << e.what() << std::endl;
  }
}

output:

$ g++ -std=c++11 main.cpp && ./main
terminate called after throwing an instance of 'binary_exc<derived_exc2, derived_exc1>'
Aborted (core dumped)

Instead of:

$ g++ -std=c++11 main.cpp && ./main
base_exc: something occured

What I'm trying to achieve: I would like to have two 'orthogonal' classification criteria for certain exceptions in my code, e.g one based on the location in the code (library1_exc, library2_exc, ...) and one based on categories of errors (myobject1isoutofbounds_exc, myobject2isbroken_exc, .. ).

These objections could be thrown using something like throw binary_exc<library2_exc, myobject1isoutofbounds_exc>(msg) and I would be able to catch them using either:

  • the first derived class catch(library2_exc const& e)
  • the second derived class catch(myobject1isoutofbounds_exc const& e)
  • the base class catch(base_exc const& e)

With my code above the first two - catching with derived classes - work fine, but the last one doesn't. Why? Is there an anti-pattern here?

Note that:


Solution

  • The boost document you linked is your exact problem. The conversion from binary_exc to base_exc is ambiguous, and thus the exception handler doesn't match.

    When virtual inheritance isn't employed, an object of type binary_exc<derived_exc1, derived_exc2> has two base_exc sub-objects. It's laid out like this:

    +----------------------------------------+
    |   +--------------+  +--------------+   |
    |   | +----------+ |  | +----------+ |   |
    |   | | base_exc | |  | | base_exc | |   |
    |   | +----------+ |  | +----------+ |   |
    |   | derived_exc1 |  | derived_exc2 |   |
    |   +--------------+  +--------------+   |
    | binary_exc<derived_exc1, derived_exc2> |
    +----------------------------------------+
    

    Since there are two base_exc sub-objects, the binary_exc object can't be bound to a reference to base_exc. How would the compiler know which base_exc object to bind the reference to?

    In fact, it doesn't work for the exact same reason the following doesn't compile:

    struct base {};
    struct derived1 : base {};
    struct derived2 : base {};
    struct derived3 : derived1, derived2 {};
    
    void foo(const base& b) {}
    
    int main() {
        derived3 d3;
        foo(d3);
    }
    

    The solution is to use virtual inheritance:

    struct base_exc : std::runtime_error
    {
      base_exc(const std::string& s): std::runtime_error(("base_exc: " + s).c_str()){}
    };
    
    struct derived_exc1 : virtual base_exc // <--- NOTE: added virtual keyword
    {
      derived_exc1(const std::string& s): base_exc(("derived_exc1: " + s).c_str()){}
    };
    
    struct derived_exc2 : virtual base_exc // <--- NOTE: added virtual keyword
    {
      derived_exc2(const std::string& s): base_exc(("derived_exc2: " + s).c_str()){}
    };
    
    template <typename T1, typename T2>
    struct binary_exc: T1, T2
    {
      binary_exc(const std::string& s): base_exc(s), T1(s), T2(s){} // <-- NOTE: added call to base_exc constructor
    };
    

    Live Demo

    Using virtual inheritance, binary_exc will have only one base_exc sub-object. It will be laid out like this:

    +------------------------------------------------+
    | +----------+ +--------------+ +--------------+ |
    | | base_exc | | derived_exc1 | | derived_exc2 | |
    | +----------+ +--------------+ +--------------+ |
    |     binary_exc<derived_exc1, derived_exc2>     |
    +------------------------------------------------+
    

    Since there's only one base_exc sub-object, the conversion is no longer ambiguous, so a binary_exc object can be bound to a reference to base_exc.

    Note that because binary_exc is required to initialize base_exc, at least one of the template type parameters must be a class derived from base_exc. You could use some SFINAE tricks to avoid this, but that's something for another question.