17

new在分配大块内存时,运算符(或对于 POD,malloc/calloc)支持一种简单而有效的失败形式。

假设我们有这个:

const size_t sz = GetPotentiallyLargeBufferSize(); // 1M - 1000M
T* p = new (nothrow) T[sz];
if(!p) {
  return sorry_not_enough_mem_would_you_like_to_try_again;
}
...

std::containers 是否有任何此类构造,或者我是否总是必须std::vector与朋友一起处理(预期的!!)异常?


是否有一种方法可以编写一个自定义分配器来预分配内存,然后将此自定义分配器传递给向量,这样只要向量不要求比您预先放入分配器更多的内存,它就不会失败?


事后思考bool std::vector::reserve(std::nothrow) {...}:除了正常的储备函数之外,真正需要的是成员函数。但是,由于只有在分配器也被扩展以允许 nothrow 分配时,这才有意义,所以它不会发生。似乎(nothrow)新的东西毕竟对某些东西有好处:-)


编辑:至于我为什么要问这个:

我在调试时想到了这个问题(调试器的第一次机会/第二次机会异常处理):如果我将调试器设置为第一次机会捕获任何 bad_alloc,因为我正在测试低内存条件,如果它还捕获了那些已经在代码中很好预期和处理的 bad_alloc 异常。这不是/不是一个真正的大问题,但我突然想到,异常是针对特殊情况的,而我已经预料到代码中每个奇怪的调用都会发生的事情并不例外。

如果new (nothrow)有它的合法用途,那么 vector-nothrow-reserve 也会有。

4

3 回答 3

16

默认情况下,标准的 STL 容器类使用底层的std::allocator类来进行分配,这就是为什么std::bad_alloc如果没有可用内存它们可以抛出的原因。有趣的是,关于分配器的 C++ ISO 规范指出,任何分配器类型的返回值都必须是指向能够容纳一定数量元素的内存块的指针,这会自动阻止您构建可能使用以下nothrow版本的自定义分配器new有这些静默分配失败。但是,您可以构建一个自定义分配器,如果没有可用内存则终止程序,因为那时返回的内存在没有剩余内存时是有效的,这是空洞的。:-)

简而言之,标准容器默认抛出异常,并且您尝试使用自定义分配器自定义它们以防止抛出异常的任何方式都不符合规范。

于 2011-01-28T10:04:23.567 回答
6

我们经常听到“我不想使用异常,因为它们效率低下”。

除非您指的是希望关闭所有运行时类型信息的“嵌入式”环境,否则如果以适当的方式抛出异常,则不应过分担心异常的效率低下。内存不足是这些适当的方法之一。

vector 的部分约定是,如果无法分配,它将抛出。如果您编写一个返回 NULL 的自定义分配器,那会更糟,因为它会导致未定义的行为。

如果您必须使用分配器,它将首先尝试失败的分配回调(如果有可用的回调),并且只有在您仍然无法分配到 throw 时才会尝试,但您仍然必须以异常结束。

我可以给你一个提示吗:如果你真的要分配如此大量的数据,那么 vector 可能是错误的类,你应该使用 std::deque 。为什么?因为双端队列不需要连续的内存块,但仍然是恒定时间查找。优点有两个:

    • 分配失败的频率会降低。因为您不需要连续的块,所以您很可能拥有可用的内存,尽管不在单个块中。
    • 没有重新分配,只是更多的分配。重新分配很昂贵,因为它需要移动所有对象。当您处于高容量模式时,可以非常及时地进行操作。

当我过去在这样的系统上工作时,我们发现由于上面的原因 1,我们实际上可以使用 deque 存储的数据量是使用 vector 的 4 倍以上,并且由于原因 2 它更快。

我们做的其他事情是分配一个 2MB 的备用缓冲区,当我们捕获到 bad_alloc 时,我们释放了缓冲区,然后无论如何都扔了,以表明我们已经达到了容量。但是现在有了 2MB 的备用空间,我们至少知道我们有内存来执行小操作,将数据从内存移动到临时磁盘存储。

因此,我们有时可以捕获 bad_alloc 并采取适当的措施来保持一致的状态,这是异常的目的,而不是假设内存耗尽总是致命的,除了终止程序(或者更糟糕的是,调用未定义的行为)。

于 2011-01-28T10:58:26.187 回答
3

标准容器为此使用异常,除了仅在知道它会成功后尝试分配之外,您无法绕过它。你不能以便携的方式做到这一点,因为实现通常会过度分配一个未指定的数量。如果您必须在编译器中禁用异常,那么您对容器的操作将受到限制。

关于“简单高效”,我认为std容器相当简单且相当高效:

T* p = new (nothrow) T[sz];
if(!p) {
    return sorry_not_enough_mem_would_you_like_to_try_again;
}
... more code that doesn't throw ...
delete[] p;

try {
    std::vector<T> p(sz);
    ... more code that doesn't throw ...
} catch (std::bad_alloc) {
    return sorry_not_enough_mem_would_you_like_to_try_again;
}

它的代码行数相同。如果它在故障情况下出现效率问题,那么您的程序必须每秒失败数十万次,在这种情况下,我稍微质疑程序设计。new我还想知道在什么情况下,与可能确定它不能满足请求的系统调用的成本相比,抛出和捕获异常的成本是显着的。

但更好的是,编写你的 API 来使用异常怎么样:

std::vector<T> p(sz);
... more code that doesn't throw ...

比原始代码短四行,当前必须处理“sorry_not_enough_mem_would_you_like_to_try_again”的调用者可以处理异常。如果此错误代码通过几层调用者向上传递,则您可以在每一层保存四行。C++ 有例外,对于几乎所有目的,您不妨接受这一点并相应地编写代码。

关于“(预期!!)” - 有时您知道如何处理错误情况。在这种情况下要做的是捕获异常。这就是异常应该如何工作的方式。如果抛出异常的代码以某种方式知道没有任何人可以捕获它,那么它可以终止程序。

于 2011-01-28T11:12:29.697 回答