从C ++ 11标准(15.1.p4):
异常对象的内存以未指定的方式分配 方式,除了3.7.4.1
中所述
如果分配失败怎么办 - 它会抛出std::bad_alloc
吗?致电std::terminate
?未指定?
答案 0 :(得分:9)
(提供我自己的答案......我等待几天,如果没有问题 - 我会将其标记为已接受)
我花了一些时间来研究这个,这就是我出土的地方:
Itanimum ABI建议将堆用于例外:
抛出异常需要存储。这个存储必须 堆栈被解开时仍然存在,因为它将被使用 处理程序,并且必须是线程安全的。异常对象存储将 因此通常在堆中分配
...
内存将由
__cxa_allocate_exception
运行时库例程分配。
所以,是的...抛出异常可能涉及锁定互斥锁并搜索空闲内存块。 : - (
它还提到了这个:
如果__cxa_allocate_exception无法在这些约束下分配异常对象,则调用terminate()
是的...在GCC和Clang"抛出myX();"可以杀死你的应用程序,你不能做任何事情(可能写自己的__cxa_allocate_exception
可以帮助 - 但它肯定不会是便携式的)
它变得更好:
3.4.1分配异常对象
异常对象的内存将由。分配 __cxa_allocate_exception运行时库例程,具有第2.4.2节中描述的一般要求。如果正常分配 失败,然后它会尝试分配一个紧急缓冲区, 在3.3.1节中描述,在以下约束条件下:
- 异常对象大小(包括标题)小于1KB。
- 当前线程尚未包含四个缓冲区。
- 保留缓冲区的线程少于16个,或者此线程 将等到其他人释放缓冲区之后再获取缓冲区。
是的,你的程序可以简单地挂起!这种情况很小 - 您需要耗尽内存,并且您的线程需要耗尽所有16个紧急缓冲区并进入等待另一个应该生成异常的线程。但是如果你用std::current_exception
做事(比如链接异常并在线程之间传递它们) - 它就不是那么不可能了。
<强>结论:强>
这是C ++标准的一个缺陷 - 您无法编写100%可靠的程序(使用例外)。教科书示例是一个服务器,它接受来自客户端的连接并执行提交的任务。处理问题的明显方法是抛出异常,这将解除所有问题并关闭连接 - 所有其他客户端都不会受到影响,服务器将继续运行(即使在低内存条件下)。唉,这样的服务器不可能用C ++编写。
你可以声称现代系统(即Linux)会在我们达到这种情况之前杀死这样的服务器。但是(1)它不是一个论点; (2)内存管理器可以设置为overcommit; (3)在具有足够内存的64位硬件上运行的32位应用程序(或者如果app人为限制内存分配),OOM杀手不会被触发。
就个人而言,我对这一发现感到非常生气 - 多年来我声称我的代码优雅地处理了内存不足。结果我骗了我的客户。 :-(不妨开始拦截内存分配,调用std::terminate
并将所有相关函数视为noexcept
- 这肯定会让我的生活更轻松(编码方式)。难怪他们仍然使用Ada来节目火箭。
答案 1 :(得分:2)
[intro.compliance] / 2 虽然本国际标准仅规定了对C ++实现的要求,但如果将这些要求表达为对程序,程序部分或执行的要求,则这些要求通常更容易理解。的计划。这些要求具有以下含义:
(2.1) - 如果某个程序不违反本国际标准中的规则,则符合要求的实施应在其资源限制内,接受并正确执行该程序。
强调我的。基本上,标准设想无法分配动态内存(并在这种情况下规定行为),但没有任何其他类型的内存;并且没有以任何方式规定在达到资源限制时实施应该做什么。
另一个例子是由于递归太深而耗尽了堆栈。标准没有说明允许递归的深度。产生的堆栈溢出是在资源限制内行使其#34;的实现。从右到失败。
答案 2 :(得分:2)
目前的答案已经描述了GCC的作用。我检查了MSVC行为 - 它在堆栈上分配异常,因此分配不依赖于堆。这使得堆栈溢出成为可能(异常对象可能很大),但标准C ++不包含堆栈溢出处理。
我用这个简短的程序来检查异常抛出期间发生了什么:
#include <iostream>
class A {
public:
A() { std::cout << "A::A() at " << static_cast<void *>(this) << std::endl; }
A(const A &) { std::cout << "A::A(const A &) at " << static_cast<void *>(this) << std::endl; }
A(A &&) { std::cout << "A::A(A &&) at " << static_cast<void *>(this) << std::endl; }
~A() { std::cout << "A::~A() at " << static_cast<void *>(this) << std::endl; }
A &operator=(const A &) = delete;
A &operator=(A &&) = delete;
};
int main()
{
try {
try {
try {
A a;
throw a;
} catch (const A &ex) {
throw;
}
} catch (const A &ex) {
throw;
}
} catch (const A &ex) {
}
}
使用GCC输出进行构建时,清楚地表明抛出的异常远离堆栈:
A::A() at 0x22cad7
A::A(A &&) at 0x600020510
A::~A() at 0x22cad7
A::~A() at 0x600020510
使用MSVC输出构建时,会在堆栈附近分配异常:
A::A() at 000000000018F4E4
A::A(A &&) at 000000000018F624
A::~A() at 000000000018F4E4
A::~A() at 000000000018F624
使用调试器进行的额外检查表明,catch处理程序和析构函数在堆栈顶部执行,因此堆栈消耗随着每个catch块的增加而增加,从第一次抛出开始直到std::uncaught_exceptions()
变为0。
这种行为意味着正确的内存处理需要你证明有足够的堆栈空间让程序在途中执行异常处理程序和所有析构函数。
为了向GCC证明相同,您似乎需要证明不超过四个嵌套异常和异常大小小于1KiB(这包括标题)。此外,如果某个线程有超过四个嵌套异常,您还需要证明紧急缓冲区分配不会导致死锁。
答案 3 :(得分:0)
Actualy指定如果异常对象的分配失败,则应抛出bad_alloc
,实现也可以调用新的处理程序。
这是您在网站[basic.stc.dynamic.allocation]的c ++标准部分(第3.7.4.1节)中指定的实际内容:
中召回无法分配存储的分配函数可以调用当前安装的新处理函数 (21.6.3.3),如果有的话。 [ 注意: 程序提供的分配功能可以获得当前的地址 安装 new_handler 使用 的std :: get_new_handler 功能(21.6.3.4)。 - 结束说明 ]如果分配 具有非抛出异常规范(18.4)的函数无法分配存储,它将返回null 指针。任何其他无法分配存储的分配函数都应仅通过抛出一个来指示失败 与类型的处理程序(18.3)匹配的类型的异常(18.1) 的std :: bad_alloc的 (21.6.3.1)。
在某些情况下,必须放弃异常处理以获得不那么微妙的错误处理技术。 [ 注意: 这些情况是: - (1.1) 当异常处理机制时,完成异常对象的初始化后但是 在激活异常处理程序(18.1)*
之前
因此,itanium ABI不遵循c ++标准规范,因为如果程序无法为异常对象分配内存,它可以阻塞或调用terminate
。