我正在为我的库设计 C++ 中的异常层次结构。“层次结构”是从 std::runtime_error 派生的 4 个类。我想避免异常类的切片问题,因此使复制构造函数受到保护。但显然 gcc 在抛出它们的实例时需要调用复制构造函数,因此抱怨受保护的复制构造函数。Visual C++ 8.0 可以很好地编译相同的代码。是否有任何可移植的方法来化解异常类的切片问题?标准是否说明了实现是否可以/应该需要要抛出的类的复制构造函数?
5 回答
您的异常需要有一个公共复制构造函数。编译器必须能够复制它才能进行异常处理。
您的问题的解决方案是始终通过引用来捕获:
try {
// some code...
throw MyException("lp0 is on fire!");
} catch (MyException const &ex) {
// handle exception
}
(const
-ness 是可选的,但我总是把它放进去,因为很少需要修改异常对象。)
Thomas 的回答是正确的,但我还想建议您不要通过“设计异常层次结构”来浪费时间。设计类层次结构是一个非常糟糕的主意,特别是当您可以从 C++ 标准异常类中简单地派生几个(仅此而已)新异常类型时。
我会避免设计与您的库不同的异常层次结构。std::exception
尽可能多地使用层次结构,并始终从该层次结构中的某些内容中获取异常。您可能想阅读 Marshall Cline 的 C++ FAQ 的异常部分-特别阅读FAQ 17.6、17.9、17.10和17.12。
至于“强制用户通过引用捕获”,我不知道有什么好的方法。我在一个小时左右的比赛中(周日下午)想出的唯一方法是基于多态投掷:
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方法中抛出异常,并使复制构造函数受到保护。(感谢 D.Shawley)
- 从库中抛出派生异常并发布异常基类以供客户端捕获。基类可能具有受保护的复制构造函数,这仅允许捕获它们的好方法。(这里提到一个类似的问题)
C++ 标准确实声明复制构造函数需要在抛出点可访问。我的配置中的 Visual C++ 8.0 通过不强制复制构造函数的存在违反了这部分标准。在第 15.1.3 节中:
throw-expression 初始化一个临时对象,其类型通过从 throw 操作数的静态类型中删除任何顶级 cv 限定符并将类型从“T 数组”或“函数返回 T”调整为分别是“指向 T 的指针”或“指向返回 T 的函数的指针”。
如果除了执行与使用临时对象相关的构造函数和析构函数(12.2)之外,可以在不改变程序含义的情况下消除临时对象的使用,则可以直接使用参数初始化处理程序中的异常的 throw 表达式。当抛出的对象是一个类对象,并且用于初始化临时副本的复制构造函数不可访问时,程序是病态的(即使临时对象可以被消除)
该答案由 OP 发布到问题中,我将其从问题中删除并作为单独的答案发布。
我想说不要使用任何内置的 C++ 异常代码。如果您必须有例外,请从头开始创建自己的例外。这是确保它们行为相似的唯一方法,更不用说以类似的方式实现了,并且直言不讳地,C++ 中异常的实现是不称职的。