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:
catch(library2_exc const& e)
catch(myobject1isoutofbounds_exc const& e)
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:
binary_exc
produces the same result. (Edit: what I meant when I wrote the ticket was that I tried struct binary_exc: virtual T1, virtual T2
)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
};
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.