Egor Tensin
Egor Tensin

Reputation: 608

C++ exception inheritance ambiguity

Why does this work?

#include <exception>
#include <iostream>
#include <stdexcept>

#include <boost/exception/all.hpp>

struct foo_error : virtual boost::exception, public std::runtime_error
{
    explicit foo_error(const char* what)
        : std::runtime_error(what)
    { }

    explicit foo_error(const std::string& what)
        : std::runtime_error(what)
    { }
};

struct bar_error : virtual boost::exception, public std::runtime_error
{
    explicit bar_error(const char* what)
        : std::runtime_error(what)
    { }

    explicit bar_error(const std::string& what)
        : std::runtime_error(what)
    { }
};

struct abc_error : virtual foo_error, virtual bar_error
{
    explicit abc_error(const char* what)
        : foo_error(what), bar_error(what)
    { }

    explicit abc_error(const std::string& what)
        : foo_error(what), bar_error(what)
    { }
};

static void abc()
{
    throw abc_error("abc error");
}

int main()
{
    try
    {
        abc();
    }
    catch (const std::exception& e)
    {
        std::cerr << e.what();
    }
}

I thought this shouldn't compile due to the ambiguous conversion from abc_error to std::exception. What am I missing? I came up with the inheritance diagram, and I can't really figure out why this code works (the arrows denote virtual inheritance and the lines denote non-virtual inheritance).

  std::exception                  std::exception
        +                               +
        |                               |
        |                               |
        +                               +
std::runtime_error              std::runtime_error
        +                               +
        |                               |
        |   +-->boost::exception<-+     |
        +   |                     |     +
   foo_error+<-----+         +--->+bar_error
                   |         |
                   |         |
                   |         |
                   +abc_error+

It looks like abc_error includes two instances of std::exception so the catch (or so I thought) shouldn't be able to cast abc_error to std::exception. Or should it?

UPDATE

I can't answer my own questions at the moment, so I'm going to continue here. I've narrowed the problem down to:

struct NonVirtualBaseBase { };
struct NonVirtualBase : NonVirtualBaseBase { };

struct VirtualBase { };

struct A : virtual VirtualBase, NonVirtualBase { };
struct B : virtual VirtualBase, NonVirtualBase { };

struct C : A, B { };

int main()
{
    try
    {
        throw C();
    }
    catch (const VirtualBase& e)
    {
        return 1;
    }

    return 0;
}

The sample above works as expected and is a perfectly fine piece of code. It crashes if I replace catch (const VirtualBase& e) with catch (const NonVirtualBase& e) which I think is sane and makes sense. But it also works if I replace the same line with catch (const NonVirtualBaseBase& e) which to me seems odd and wrong. A compiler bug?

Upvotes: 5

Views: 556

Answers (3)

Sneftel
Sneftel

Reputation: 41464

It has to compile, because there's no reason you can't have multiply-defined base classes in exceptions, and no reason you can't have that base class in an exception declaration somewhere. The fact that abc happens to throw a particular thing, and that main happens to catch a particular thing, and that those things are frenemies, cannot be compile-time checked.

What it WON'T do is catch the exception properly, or at least it shouldn't. But I can see how a particular compiler might end up (incorrectly) doing that, because of how exception declarations are matched.

Upvotes: 1

Agentlien
Agentlien

Reputation: 5116

UPDATE

As pointed out by the OP, this explanation doesn't quite cut it, since std::exception is not derived from using virtual inheritance. I believe the answer to be that this is actually undefined behaviour and simply not caught at compile time because there is no need for the throw and catch to know of each other and warn if they are incompatible.

END UPDATE

The answer is that this hierarchy uses*virtual inheritance* to derive from boost::exception.

Since both foo_error and bar_error uses virtual inheritance to inherit from boost::exception, there will only be a single boost::exception base which is shared between both the foo_error and bar_error sub-objects of an abc_error.

When you specify virtual before an entry in the list of base classes, this means all occurences of this class as a virtual base class in the most derived object will actually refer to the same instance. It is used specifically to avoid ambiguity in this type of design.

Upvotes: 2

n. m. could be an AI
n. m. could be an AI

Reputation: 119847

Why does this work?

For some loosely defined values of "work".

me@mybox > g++ -o test test.cpp
me@mybox > ./test
terminate called after throwing an instance of 'abc_error'
Aborted (core dumped)
me@mybox >

Upvotes: 1

Related Questions