在 C++ 的异常层次结构中引入额外的基类型?

Introduce an additional base type in an exception hierarchy in C++?

我们的基本情况如下所示:

// 3rd party lib:

namespace ns3p {
  class OperationException : public std::exception; // yes, no `virtual` here
}

// our code:

// wrapper with additional information to the ns3p::OperationException
class ModuleOpException : public ns3p::OperationException;

// our catch sites:
// mostly:
catch (ModuleOpException const& ex) { ...
// but sometimes:
catch (ns3p::OperationException const& ex) { ...

现在,这增加了额外的异常,所有异常都来自 ModuleOpException 与来自第 3 方库的任何错误没有任何关系,但是只是在使用第 3 方库的内容的相同上下文中抛出。

// This indirectly derives from ns3p::OperationException, even though 
// there is *no* underlying OperationException at all. It just thrown "stand alone"
// and caught by ModuleOpException& :
class InvalidXYZException : public ModuleOpExcpetion;

我们现在已经考虑 "inverting" 层次结构以更好地反映实际情况,并且这样做(最初)对其他代码的影响最小。

我们计划这样做:

// new base exception type:
class ModuleBaseException : public virtual std::exception;

// changed to derive both from our base type as well as the 3rd party type:
class ModuleOpException : public virtual ModuleBaseException, public virtual ns3p::OperationException;

// only derives from the base type:
class InvalidXYZException : public virtual ModuleBaseException;

// all catch sites change the catch of `ModuleOpException` to:
catch (ModuleBaseException const& ex) { ...
// and leave the catch ns3p::OperationException alone

这应该有效(应该吗?),除了我不确定第三部分异常类型的 std::exception 的非虚拟继承有多少会把事情搞砸。我认为我们可以安全as long as noone tries to catch(std::exception const&) in which case the catch would fail在运行时绑定,因为转换不明确。

这看起来是一个可行的解决方案吗?或者是 "really bad idea" 尝试将 non-virtual-std::exception 类型与上面的层次结构集成?

注意:我们可以更改第三方库以从 [=] 派生 "properly" 和 virtual 的可能性为零(如 0.00%) 43=].


try...catch 应该发生在异常将被正确处理的地方,或者如果异常将在此时被修改并重新抛出。

使用不同异常的目的类是为了以不同的方式处理它们。 (我的意见是,这是一个坏主意,打开某种代码会更好,并且您可能仅在极少数情况下才根据类型更改您的操作,例如如果错误是暂时性的,则重试请求)。

无论如何,我认为第 3 方库用于帮助实现您自己的库。您的库的用户不想知道您用于实现的库的异常。

因此,您应该从您的库的用户那里抽象出来,以用户期望的方式捕获它们中的任何一个并将它们作为您自己的重新抛出。

从您的库中抛出的异常与您函数中的 return 值一样,都是 "contract" 的一部分。

因此,用户可能希望捕获 ModuleBaseException 或 ModuleOpException 而不是 ns3p::OperationException。

您自己的代码应该捕获它们,可能会将它们转换为您的 ModuleOpException 类型并抛出它。

恕我直言,应该可以。 C++ 规范说:

处理程序匹配 E 类型的异常对象,如果...处理程序是 cv T 或 cv T& 类型,并且 T 是明确的 public 基 class 或 ...

try 块的处理程序按出现顺序进行尝试。

因此,只要您在 ns3p::OperationException 之前捕获 ModuleBaseException,任何源自 ModuleBaseException 的异常都应该在正确的处理程序中捕获,即使它也源自 ns3p::OperationException

唯一的问题是来自 std::exception 的所有方法和字段将在您的异常 classes 中重复,您应该始终将它们限定为来自 ModuleBaseException 以避免歧义。您可能应该使用通用实用程序来处理将在所有处理程序中使用的异常。

从现有异常层次结构派生时,您有多重继承(一个用于您自己的,一个用于现有层次结构)。为避免歧义,您可以传递并保留指向现有层次结构的指针:

#include <stdexcept>
#include <sstream>

// Error
//=============================================================================

/// Base class for all exceptions in a library.
/// \NOTE The constructor requires a std::exception as argument
class Error
{
    // Construction
    // ============

    public:
    virtual ~Error() noexcept {};

    protected:
    Error(std::exception& exception)
    :   m_exception(&exception)
    {}


    // Element Access
    // ==============

    public:
    const char* msg() const { return m_exception->what(); }

    // Cast
    // ====

    protected:
    template <typename Derived>
    static std::exception& cast(Derived& derived) {
        return static_cast<std::exception&>(derived);
    }

    private:
    std::exception* m_exception;
};


// Runtime Errors
// ============================================================================

/// A Runtime Error.
class ErrorRuntime : public std::runtime_error, public Error
{
    public:
    explicit ErrorRuntime(const std::string& msg)
    :   std::runtime_error(msg), Error(cast(*this))
    {}
};

// Test
// ====

#include <iostream>

int main()
{
    try {
        throw ErrorRuntime("Hello Exception");
    }
    catch(const std::exception& e) {
        try {
            std::cerr << "std::exception: " << e.what() << "\n";
            throw;
        }
        catch(const Error& e) {
            try {
                // No what here
                std::cerr << "Error: " << e.msg() << "\n";
                throw;
            }
            catch(const ErrorRuntime& e) {
                std::cerr << "ErrorRuntime: " << e.what() << "\n";
            }
        }
    }
}

但是,如果您有第三方库,您可能会隐藏该库的所有异常(如其他人所述)。隐藏成本是传入和传出异常的两层。