4

I understand that to properly catch exceptions using multiple inheritance I need to use virtual inheritance.
I'm not necessarily advocating the controversial use of multiple inheritance, but I do not want to design systems that make its usage impossible. Please do not distract from this question to advocate or attack the use of multiple inheritance.

So let's say I have the following base exception type:

class BaseException : public virtual std::exception {
public:
    explicit BaseException(std::string msg)
    : msg_storage(std::make_shared<std::string>(std::move(msg))) { }

    virtual const char* what() const noexcept { return msg_storage->c_str(); }

private:
    // shared_ptr to make copy constructor noexcept.
    std::shared_ptr<std::string> msg_storage;
};

What is the proper way to create an exception hierarchy from BaseException?


My problem lies with constructing the exception types. Ideally every exception just constructs its parent, but this is impossible due to virtual inheritance. One solution would be to construct every parent up the chain:

struct A : public virtual BaseException {
    explicit A(const std::string& msg) : BaseException(msg) { }
};

struct B : public virtual A {
    explicit B(const std::string& msg, int code)
    : BaseException(msg), A(msg), code_(code) { }

    virtual int code() const { return code_; }

private:
    int code_;
};

struct C : public virtual B {
    explicit C(const std::string& msg, int code)
    : BaseException(msg), A(msg), B(msg, code) { }
};

But this seems very repetitive and error-prone. Furthermore, this makes it impossible for constructors of exception types to add/change information passed in by their children before passing through to their respective parents.

Community
  • 1
  • 1
orlp
  • 112,504
  • 36
  • 218
  • 315
  • 1
    I wonder if all of this will really be useful at the end of the day. Do you have code or expect clients to have code that catches more than just `std::exception` or `BaseException`, and act differently according to what kind of exception was thrown? – Christian Hackl Mar 14 '15 at 19:54
  • There's nothing controversial about multiple inheritance, but to be used effectively, the virtual base classes should be abstract, and not contain any data members. – James Kanze Mar 14 '15 at 19:54
  • @ChristianHackl Yes, client code might want to catch `IOError` but not `MemoryError` as an example. Either way - I'm not interested in discussing the usefulness of an exception hierarchy, merely the proper way to implement one in C++. I'm not interested in alternatives, or why what I'm doing is a bad idea. Sorry for sounding aggressive, but the SO crowd has the annoying tendency to think outside of the box, when I'm explicitly interested in an answer inside the theoretical box. – orlp Mar 14 '15 at 20:04
  • 1
    Have you looked at [Boost.Exception](http://www.boost.org/doc/libs/1_57_0/libs/exception/doc/boost-exception.html) and the discussions in its documentation? – Angew is no longer proud of SO Mar 14 '15 at 20:09
  • 4
    @orlp In this case, I don't think anyone is "thinking outside the box". Your question (albeit not for exception hierarchies, but for inheritance in general) was seriously considered during the standardization process. In the end, it was felt that since there was no valid use for virtual inheritance of classes with data members, there was no point in complicating the language in order to solve the problem. – James Kanze Mar 14 '15 at 20:09
  • @orlp: I think comments are the right place for thinking outside of the box. Answers, in contrast, should certainly be more focused on what you call thinking "inside the theoretical box", especially if the OP obviously knows what he or she is talking about (as is the case with this question). I could understand how "outside of the box" answers could be annoying, but comments? – Christian Hackl Mar 14 '15 at 21:23
  • @orlp: I still don't see what your use case is. – quamrana Mar 14 '15 at 22:30

1 Answers1

2

I have found a reasonable solution that's not broken as far as I can see. It uses a slightly modified base exception type:

struct BaseException : virtual std::exception {
    explicit BaseException(std::string msg)
    : msg_storage(std::make_shared<std::string>(std::move(msg))) { }

    virtual const char* what() const noexcept { return msg_storage->c_str(); }

protected:
    BaseException();

private:
    std::shared_ptr<std::string> msg_storage;
};

Then the rules are:

  1. Every exception inherits publicly and virtually from its parent exceptions.

  2. Every exception declares a protected default constructor and defines a protected constructor initializing all data members.

  3. Every exception that is supposed to be Constructible defines a public constructor that directly calls the constructors defined in 2 for every ancestor.

  4. All copy constructors should be noexcept.


An example hierarchy using this:

// A regular derived exception.
struct RuntimeError : virtual BaseException {
    RuntimeError(std::string msg) : BaseException(std::move(msg))  { }
protected: RuntimeError() { }
};

// Derived exception with data member.
struct OSError : virtual RuntimeError {
    OSError(std::string msg, int e) : BaseException(std::move(msg)), e(e)  { }
    virtual int error_code() const noexcept { return e; }

protected:
    OSError();
    OSError(int e) : e(e) { }

private:
    int e;
};

// Non-constructible exception type.
struct FatalError : virtual RuntimeError {
protected: FatalError() { }
};

// A composed exception type.
struct FatalOSError : virtual FatalError, virtual OSError {
    FatalOSError(std::string msg, int e)
    : BaseException(std::move(msg)), OSError(e)  { }
protected: FatalOSError() { }
};
orlp
  • 112,504
  • 36
  • 218
  • 315