std::runtime_error::runtime_error(const std::string&)如何满足std::exception对throw()的要求?

17
标准库的异常处理类std::exception要求其构造函数必须是throw()类型的。然而,std::runtime_error接受一个std::string作为其参数,这表明它在某个地方存储了一个std::string。因此,必然存在赋值或复制构造操作。而对于std::string来说,这不是一个nothrow操作。
那么,runtime_error::runtime_error如何满足throw()呢?
(为了背景,我正在实现一个异常类型,并想从调用站点存储几个std::string,并希望正确地执行。)

不需要深入研究库代码,一个简单的解决方案是在分配失败时捕获抛出的异常,并回退到一些安全的替代方案,例如内置于类型中的小缓冲区并截断结果。 - Dennis Zickefoose
@Dennis:这也行不通,因为在进入runtime_error构造函数之前,string会被默认构造。 - Billy ONeal
你假设它直接存储一个字符串,而不仅仅是字符对象的数组。此外,作为库函数,他们知道一个字符串在默认构造时是否会抛出异常。 - Dennis Zickefoose
@billy,也可以使用正确对齐的char数组,其大小为sizeof(std::string),然后将字符串放置到该数组中,通过捕获异常来实现。这样可以在不需要使用库实现细节的情况下检测string构造函数是否会抛出异常。 - bdonlan
2个回答

9

(这里有一个相同的最小测试用例。)


runtime_error::runtime_error(string const&)不需要满足throw()

它不继承或覆盖exception::exception(),并且在调用string的复制构造函数时,exception::exception()已经完成。

如果复制string引发异常,则会解开runtime_error::runtime_error(string const&),然后可能调用exception::~exception()


很难直接表明派生构造函数没有满足基类构造函数的异常说明的要求,但是下面的段落强烈暗示了这一点(它描述了如何调用基类的析构函数,而不是将异常传递到基类构造函数中):
“一个部分构造或部分销毁的对象将会为所有完全构造的子对象执行析构函数,也就是说,对于构造已经完成且析构还未开始执行的子对象。如果自动数组的元素的构造函数抛出异常,则只有该数组的已构造元素将被销毁。如果对象或数组是在new表达式中分配的,则调用匹配的释放函数(3.7.3.2、5.3.4、12.5),如果有的话,以释放所占用的存储空间。”
唯一与你假设的场景(我最初也假设)相近的段落如下。

[2003: 15.4/3] 如果虚函数有异常说明,那么任何派生类中覆盖该虚函数的所有声明(包括定义)只允许基类虚函数异常说明所允许的异常。

但显然,exception::exception()不是虚函数,而且runtime_error::runtime_error(string const&)也没有重写它。

(请注意,这种情况适用于虚析构函数;因此,您可以看到,在libstdc++中,runtime_error::~runtime_error()throw()。)


哦,所以 std::exception 必须是 nothrow 的,但是从 std::exception 派生的类可能会在它们的构造函数中抛出异常? - Billy ONeal
1
默认构造函数(和复制构造函数)必须是nothrow;接受字符串的构造函数则不应该是。看起来你正在使用默认构造函数的签名来引用所有构造函数? - Potatoswatter
1
关于更新,请参见§18.8.1/2:“每个从类异常派生的标准库类T都应该有一个公开可访问的复制构造函数和一个公开可访问的复制赋值运算符,它们不会因为异常而退出。” - Potatoswatter
4
我很不喜欢他们建议将评论讨论转移到聊天中。评论是阐述和解决问题的完美场所,并且可以保留记录。Gr. - GManNickG
4
@GMan:我完全同意你的看法。我也讨厌看到“让我们在聊天中继续讨论”,当我真的不想要时。这并不是技术上强制执行的,但“聊天”感觉像是实时交互,而我来Stack Overflow就不是为了实时交互。因此,这个功能让我感到很急迫。 - Lightness Races in Orbit
显示剩余5条评论

7

2015年更新:

然而,std::runtime_error接受一个std::string作为其参数,这表明它在某个地方存储了一个std::string。因此,必须进行赋值或复制构造。对于std::string,这不是一个noexcept操作。

runtime_error(和logic_error)只需要接受一个类型为std::string const &的参数。它们不需要复制它。

使用这些重载要自行承担风险。LLVM libc++不提供存储空间。

另一方面,GNU libstdc++小心翼翼地避免内存不足。它将字符串的内容复制到异常存储空间中,而不是到一个新的std::string中。

即使如此,它也添加了一个std::string&&重载,并使用friend关系来采用通过rvalue传递的std::string参数的内部缓冲区,以节省异常存储空间。

所以这就是你真正的答案:“如果有的话,那就非常小心。”

你可以利用GCC的慷慨,使用std::runtime_error作为自己异常类的成员,每个存储一个字符串。但是在Clang上这仍然无用。


原始答案,2011年。现在仍然适用:

堆栈展开期间发生异常会导致调用terminate

但是抛出对象的构造不是展开的一部分,并且与throw表达式之前的代码没有区别。

如果std::runtime_error::runtime_error(std::string const &)引发std::bad_alloc,则runtime_error异常将丢失(它从未存在),并且将处理bad_alloc

演示:http://ideone.com/QYPj3

至于存储来自调用点的std::string的您自己的类,您需要遵循§18.8.1/2:

派生自类异常的每个标准库类T都必须具有公共可访问的复制构造函数和公共可访问的复制赋值运算符,它们不会以异常退出。

这是必需的,因为从堆栈复制到线程的异常存储对异常很敏感。§15.1/7:

如果异常处理机制在完成评估要抛出的表达式之后,但在捕获异常之前调用一个通过异常退出的函数,则会调用std::terminate(15.5.1)。
因此,在第一次复制后应使用shared_ptr 或类似方法来清理副本。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接