【问题标题】:Avoid slicing of exception types (C++)避免对异常类型进行切片 (C++)
【发布时间】:2009-12-06 16:08:38
【问题描述】:

我正在为我的库设计一个 C++ 中的异常层次结构。 “层次结构”是从 std::runtime_error 派生的 4 个类。我想避免为异常类使用slicing problem,从而使复制构造函数受到保护。但显然 gcc 在抛出它们的实例时需要调用复制构造函数,因此抱怨受保护的复制构造函数。 Visual C++ 8.0 可以很好地编译相同的代码。是否有任何可移植的方法来化解异常类的切片问题?标准是否说明了实现是否可以/应该需要要抛出的类的复制构造函数?

【问题讨论】:

  • 几乎在任何可以使用复制构造函数的地方,C++ 标准都允许省略它的使用。但是如果你希望你的代码是可移植的,异常就必须有一个可公开访问的复制构造函数。
  • 另外,我不知道您使用的是哪个版本的 VC++,但是您的 VC++ 6.0 确实需要一个复制构造函数 - 我刚刚测试过。

标签: c++ exception gcc derived object-slicing


【解决方案1】:

您的异常需要有一个公共复制构造函数。编译器必须能够复制它才能进行异常处理。

您的问题的解决方案是始终通过引用来捕获:

try {
    // some code...
    throw MyException("lp0 is on fire!");
} catch (MyException const &ex) {
    // handle exception
}

const-ness 是可选的,但我总是把它放进去,因为很少需要修改异常对象。)

【讨论】:

  • 另外:总是抛出新创建的异常(或者至少是你知道它们是如何创建的异常)。不要通过可能指向派生类的解引用指针或引用抛出。
  • 好点。我更新了这个例子,给出了一个“好”投掷的例子。
  • 我明白异常应该通过引用来捕获。这正是我想通过隐藏复制构造函数来强制执行的。您的回答似乎表明库作者无法强制以正确的方式捕获客户端代码中的异常
  • @Steve: +1,如果不够清楚,这意味着如果你想追溯异常,你应该总是throw;,而不是throw ex;,即使ex 是参考上面的代码。
【解决方案2】:

Thomas 的回答是正确的,但我还想建议您不要通过“设计异常层次结构”来浪费时间。设计类层次结构是一个非常糟糕的主意,特别是当您可以从 C++ 标准异常类中简单地派生几个(仅此而已)新异常类型时。

【讨论】:

  • +1。我和尼尔在这里。如果您认为您的异常比其他异常发生得更频繁,因此需要它自己的异常类型(您可能遇到错误情况而不是异常)。否则每个功能单元 1 个例外(您如何定义功能单元含糊不清,但更大而不是更小)。
【解决方案3】:

我会避免设计与您的库不同的异常层次结构。尽可能使用std::exception 层次结构,并且始终从该层次结构中的某些内容中派生异常。您可能想阅读exceptions portion of Marshall Cline's C++ FAQ - 特别是阅读FAQ 17.617.917.1017.12

至于“强制用户通过引用捕捉”,我不知道有什么好的方法。我在一个小时左右的比赛中(周日下午)想出的唯一方法是基于polymorphic throwing

class foo_exception {
public:
    explicit foo_exception(std::string msg_): m_msg(msg_) {}
    virtual ~foo_exception() {}
    virtual void raise() { throw *this; }
    virtual std::string const& msg() const { return m_msg; }
protected:
    foo_exception(foo_exception const& other): m_msg(other.m_msg) {}
private:
    std::string m_msg;
};

class bar_exception: public foo_exception {
public:
    explicit bar_exception(std::string msg_):
        foo_exception(msg_), m_error_number(errno) {}
    virtual void raise() { throw *this; }
    int error_number() const { return m_error_number; }
protected:
    bar_exception(bar_exception const& other):
        foo_exception(other), m_error_number(other.m_error_number) {}
private:
    int m_error_number;
};

这个想法是保护复制构造函数并强制用户调用Class(args).raise() 而不是throw Class(args)。这使您可以抛出一个多态绑定异常,您的用户只能通过引用捕获该异常。任何按值捕获的尝试都应该收到很好的编译器警告。比如:

foo.cpp:59: 错误:'bar_exception::bar_exception(const bar_exception&)' 受保护

foo.cpp:103: 错误:在此上下文中

当然,这一切都是有代价的,因为您不能再显式使用throw,否则您会收到类似的编译器警告:

foo.cpp:在函数'void h()'中:

foo.cpp:31: 错误:'foo_exception::foo_exception(const foo_exception&)' 受保护

foo.cpp:93: 错误:在这个上下文中

foo.cpp:31: 错误:'foo_exception::foo_exception(const foo_exception&)' 受保护

foo.cpp:93: 错误:在这个上下文中

总体而言,我会依赖编码标准和文档说明您应该始终通过参考来了解。确保您的库捕获通过引用处理的异常并抛出新对象(例如,throw Class(constructorArgs)throw;)。我希望其他 C++ 程序员也有同样的知识——但是为了确定,请在任何文档中添加注释。

【讨论】:

  • 感谢您的宝贵时间!无论如何,库中的所有抛出都是通过宏来完成的,以便将文件名和行号编码到异常中,所以我可以更改宏定义以调用 raise。
【解决方案4】:

我发现阻止我的库的客户端按值错误捕获异常的两种可移植方法是

  1. 从异常类的virtual raise 方法内部抛出异常,并使复制构造函数受到保护。 (感谢 D.Shawley)
  2. 从库中抛出派生异常并发布异常基类供客户端捕获。基类可能具有受保护的复制构造函数,它只允许捕获它们的好方法。 (提到了here 类似的问题)

C++ 标准确实声明复制构造函数需要在抛出点可访问。我的配置中的 Visual C++ 8.0 通过不强制复制构造函数的存在违反了这部分标准。在第 15.1.3 节中:

throw 表达式初始化一个临时对象,其类型通过从 throw 操作数的静态类型中删除任何顶级 cv 限定符并从“T 数组”或“函数返回”中调整类型来确定T”分别指向“指向 T 的指针”或“指向返回 T 的函数的指针”。

如果除了执行与使用临时对象相关的构造函数和析构函数(12.2)之外,可以在不改变程序含义的情况下消除临时对象的使用,那么可以直接初始化处理程序中的异常使用 throw 表达式的参数。当抛出的对象是类对象,并且用于初始化临时副本的复制构造函数不可访问时,程序是非良构的(即使临时对象可以被消除)

此答案由 OP 发布到问题中,我将其从问题中删除并作为单独的答案发布。

【讨论】:

  • 虽然图书馆作者让图书馆用户难以做错事是明智之举——但不要颠覆标准也很重要。该标准规定异常对象必须是可复制的(15.1.5),并且明确允许按值捕获。我会犹豫不决。
【解决方案5】:

我会说不要使用任何内置的 C++ 异常代码。如果您必须有例外,请从头开始创建自己的例外。这是确保它们行为相似的唯一方法,更不用说以相似的方式实现了,而且直言不讳,C++ 中异常的实现是无能的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-08-18
    • 1970-01-01
    • 1970-01-01
    • 2018-10-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-29
    相关资源
    最近更新 更多