44

默认放置new操作符在 18.6 [support.dynamic] ¶1 中声明,带有非抛出异常规范:

void* operator new (std::size_t size, void* ptr) noexcept;

这个函数什么都不return ptr;做,所以它是合理的noexcept,但是根据 5.3.4 [expr.new] ¶15 这意味着编译器必须在调用对象的构造函数之前检查它是否返回 null:

-15-
[注意:除非使用非抛出异常规范(15.4)声明分配函数,否则通过抛出异常表示分配存储失败std::bad_alloc(第15条,第18.6.2.1条);否则它返回一个非空指针。如果分配函数声明为不抛出异常规范,则返回 null 以指示分配存储失败,否则返回非空指针。——尾注]如果分配函数返回null,则不进行初始化,不调用deallocation函数,new-expression的值为null。

在我看来(特别是针对放置new,而不是一般而言)这个空检查是一个不幸的性能损失,尽管很小。

我一直在调试一些代码,其中new在对性能非常敏感的代码路径中使用放置以改进编译器的代码生成,并且在程序集中观察到 null 检查。new通过提供使用抛出异常规范(即使它不可能抛出)声明的特定于类的放置重载,条件分支被删除,这也允许编译器为周围的内联函数生成更小的代码。说放置new函数可以抛出,即使它不能抛出,结果是明显更好的代码。

所以我一直想知道放置new案例是否真的需要空检查。它可以返回 null 的唯一方法是如果你将它传递给 null。尽管有可能并且显然是合法的,但可以这样写:

void* ptr = nullptr;
Obj* obj = new (ptr) Obj();
assert( obj == nullptr );

我不明白为什么这会有用,我建议如果程序员在使用放置之前必须明确检查 null 会更好,new例如

Obj* obj = ptr ? new (ptr) Obj() : nullptr;

有没有人需要放置new来正确处理空指针的情况?(即没有添加ptr一个有效内存位置的显式检查。)

我想知道禁止将空指针传递给默认放置new函数是否合理,如果不是,是否有更好的方法来避免不必要的分支,而不是试图告诉编译器该值不为空,例如

void* ptr = getAddress();
(void) *(Obj*)ptr;   // inform the optimiser that dereferencing pointer is valid
Obj* obj = new (ptr) Obj();

或者:

void* ptr = getAddress();
if (!ptr)
  __builtin_unreachable();  // same, but not portable
Obj* obj = new (ptr) Obj();

注意这个问题是故意标记为微优化,我并不是建议您new为所有类型重载放置以“提高”性能。这种效果在一个非常具体的性能关键案例中被注意到,并且基于分析和测量。

更新: DR 1748使使用带有新位置的空指针成为未定义行为,因此不再需要编译器进行检查。

4

1 回答 1

14

虽然除了“有没有人需要放置 new 来正确处理空指针情况?”之外,我看不到很多问题。(我没有),我认为这个案子很有趣,足以让人们对这个问题产生一些想法。

我认为标准被破坏或不完整 wrt 放置新功能和一般分配功能的要求。

如果您仔细查看引用的 §5.3.4,13,它意味着必须检查每个分配函数是否有返回的空指针,即使它不是noexcept。因此,应改写为

如果分配函数声明为不抛出异常规范并返回 null,则不应进行初始化,不应调用解除分配函数,并且 new-expression 的值应为 null。

这不会损害分配函数抛出异常的有效性,因为它们必须遵守§3.7.4.1

[...] 如果成功,它将返回存储块的起始地址,其字节长度应至少与请求的大小一样大。[...]返回的指针应适当对齐,以便可以将其转换为具有基本对齐要求(3.11)的任何完整对象类型的指针,然后用于访问分配的存储中的对象或数组(直到存储通过调用相应的释放函数显式释放)。

§5.3.4,14

[ 注意:当分配函数返回一个非空值时,它必须是一个指向已为对象保留空间的存储块的指针。假定存储块已适当对齐并具有请求的大小。[...] - 尾注]

显然,只返回给定指针的放置 new 无法合理地检查可用存储大小和对齐方式。所以,

§18.6.1.3,1关于安置新说

[...] (3.7.4) 的规定不适用于 operator new 和 operator delete 的这些保留放置形式。

(我猜他们错过了在那个地方提到§5.3.4,14。)

然而,这些段落一起间接说“如果你将垃圾指针传递给 palcement 函数,你会得到 UB,因为违反了 §5.3.4,14”。因此,由您来检查放置新位置的任何 poitner 的健全性。

本着这种精神,并通过重写 §5.3.4,13,该标准可以noexcept从放置 new 中删除,从而导致对该间接结论的补充:“......如果你通过 null,你也会得到 UB”。另一方面,与具有空指针相比,具有未对齐指针或指向太少内存的指针的可能性要小得多。

但是,这将消除检查 null 的需要,并且非常符合“不要为不需要的东西付费”的理念。分配函数本身不需要检查,因为 §18.6.1.3,1 明确说明了这一点。

为了四舍五入,可以考虑添加第二个重载

 void* operator new(std::size_t size, void* ptr, const std::nothrow_t&) noexcept;

可悲的是,向委员会提出这个建议不太可能导致改变,因为它会破坏现有的代码,依赖于放置 new 可以使用空指针。

于 2013-07-10T14:31:25.970 回答