7

Why the result is "B", I thought that it should hit first inherited class ("A")? When I ran it with class B that do not inherit anything from class A it hits first catch block, but I don't know reason for behavior like this in code below:

#include <iostream>
#include <exception>

using namespace std;

class A {};

class B : public A{};

class C : public A, public B {};

int main() {
    try {
        throw C();
    }
    catch (A a) {
        cout << "A" << endl;
    }
    catch (B b) {
        cout << "B" << endl;
    }
    catch (C c) {
        cout << "C" << endl;
    }
}   
  • 8
    There are 2 A subobjects in C, so it doesn't know which one to convert to, so it skips `catch (A)`. – HolyBlackCat Jan 06 '22 at 14:19
  • 3
    Turn up your warnings: https://godbolt.org/z/s6oWhEez3 Note this in particular: `warning: direct base 'A' inaccessible in 'C' due to ambiguity [-Winaccessible-base]` – Fred Larson Jan 06 '22 at 14:20

2 Answers2

3

For getting the behaviour you expect you should derive virtually.
And also a hint as a guideline throw by value - catch by reference

#include <iostream>
#include <exception>

class A {};

class B : virtual public A {};

class C : public virtual  A, public B {};

int main() {
    try {
        throw C();
    }
    catch (A const& a) {
        std::cout << "A" << std::endl;
    }
    catch (B const& b) {
        std::cout << "B" << std::endl;
    }
    catch (C const& c) {
        std::cout << "C" << std::endl;
    }
}
Eduard Rostomyan
  • 7,050
  • 2
  • 37
  • 76
  • 2
    Can you explain me why it is behaving like this and why your code works? – Nikola Nikolic Jan 06 '22 at 14:18
  • You have diamond inheritance which usually leads to ambiguities in the inheritance chain (you should try to avoid diamond inheritance, but here is some explanation): https://stackoverflow.com/questions/2659116/how-does-virtual-inheritance-solve-the-diamond-multiple-inheritance-ambiguit – Pepijn Kramer Jan 06 '22 at 14:21
  • For short answer see HolyBlackCat's comment. To dig deeper read @Pepijn Kramer's comment. – Eduard Rostomyan Jan 06 '22 at 14:23
  • 1
    @Eduard can you update the example to catch by const& and remove the "using namespace std;" (I was in the process of creating that example). Oh and C should inherit both virtual too – Pepijn Kramer Jan 06 '22 at 14:23
  • 1
    Throwing in virtual inheritance is always wrong. Virtual inheritance is a **design** decision; it it not a solution to a coding problem. – Pete Becker Jan 06 '22 at 14:23
  • And diamond inheritance usually already is a design issue – Pepijn Kramer Jan 06 '22 at 14:25
  • Agree with @PeteBecker on this one. If you end up with this designed you should probably reconsider the approach. – Eduard Rostomyan Jan 06 '22 at 14:25
1

Your classes are some variant of the deadly diamond of death case of multiple inheritance: the base class A is twice a base class of C: once directly, and once indirectly via B:

              A
              |\
              | \
              |  B 
              | /
              |/
              C

The consequence of this kind of inheritance graph is that your C object has two different A sub-objects. I name them in the graph for convenience:

            a1:A a2:A
              |   |
              |   |
              |  b:B 
              |  /
              | /
              c:C

As you see, if you mention the A sub-object, there is an ambiguity: is it a1 or a2, whereas for B there is no ambiguity. And this impacts the matching the exception handling. Because the standard's rules are as follows:

[except.handle]/3: A handler is a match for an exception object of type E if
— The handler is of type cv T or cv T& and E and T are the same type (ignoring the top-level cv-qualifiers), or
— the handler is of type cv T or cv T& and T is an unambiguous public base class of E, or
— ...

When you throw C, first the catch (A a) is tested: A is not the same type than C. And A is non an unambiguous public base class either. Then the next catch (B b) is tested: B is not the same type than C. But it's an unambiguous public base class of it. Match! And therefore your result.

Solution 1:

Making A a virtual base of both B and C as suggested in the other answer, ensures that there is only one unique and unambiguous A sub-object in C. This is why it works.

Solution 2

Virtual inheritance is not a silver bullet. As soon as you have non-default constructors, you must explicit the virtual constructor systematically. Moreover, you must use the virtual inheritance wherever A is a subclass. Last but not least, virtual inheritance is sometimes just not suitable for the needs (for example if two independent strands are needed).

In this last situation, you could also disambiguate by introducing an intermediary class:

class AA : public A {}; 
class C : public AA, public B {};

and then catch (AA& a) instead of catching A. This is stupid simple, but it removes the ambiguity, whereas there are still two different A sub-objects (again: only if you need them). (online demo)

Recommendation: To get a full understanding of the situation, I recommend that you try to update your example and add in each class some members that you'd initialize with a non-default constructor. ANd yes, catch the exception by reference.

Christophe
  • 68,716
  • 7
  • 72
  • 138