将空指针传递给placement new

时间:2013-07-10 12:50:49

标签: c++ micro-optimization placement-new noexcept

默认放置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这意味着编译器必须检查它没有' t在调用对象的构造函数之前返回null:

  

-15-
  [注意:除非使用非抛出异常规范(15.4)声明分配函数,否则它表示无法通过抛出std::bad_alloc异常来分配存储(第15,18.6.2.1节) );否则返回非空指针。如果使用非抛出异常规范声明分配函数,则返回null以指示无法分配存储,否则返回非空指针。 -end note ]如果分配函数返回null,则不进行初始化,不应调用释放函数,并且new-expression的值应为null。

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

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

所以我一直想知道对于展示位置new是否真的需要进行空检查。它返回null的唯一方法是将它传递给null。虽然写下来是可能的,而且显然是合法的:

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

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

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();

NB 这个问题是故意标记的微优化,我建议您为所有类型的“改进”重叠展示位置new性能。在非常具体的性能关键情况下,基于分析和测量,注意到了这种效应。

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

1 个答案:

答案 0 :(得分:13)

虽然我在那里看不到太多问题,但“有没有人需要放置新的来正确处理空指针的情况?” (我没有),我认为这个案子足够有趣,可以在这个问题上留下一些想法。

我认为标准已经破坏或不完整,因为放置新功能和要求通常是分配功能。

如果仔细查看引用的§5.3.4,13,它意味着必须检查每个分配函数的返回nullpointer,即使它不是noexcept。因此,应该重写为

  

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

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

  

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

§5.3.4,14

  

[注意:当分配函数返回null以外的值时,它必须是指向已保留对象空间的存储块的指针。假设存储块被适当地对准并且具有所请求的大小。 [...] - 注意事项]

显然,只返回给定指针的placement new不能合理地检查可用的存储大小和对齐方式。因此,

§18.6.1.3,1关于展示新的说法

  

[...](3.7.4)的规定不适用于运营商新的和运营商删除的这些保留的安置形式。

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

然而,这些段落一起说间接“如果你将垃圾指针传递给了附加功能,你会得到UB,因为违反了§5.3.4,14”。因此,你应该检查任何poitner给予安置新的理智。

本着这种精神,并且通过重写的§5.3.4,13,标准可以将noexcept从新的位置剥离,从而导致对该间接结论的补充:“......如果你传递null ,你也得到了UB“。另一方面,与使用空指针相比,它更不可能有一个未对齐的指针或指向内存太少的指针。

然而,这将消除检查null的必要性,并且它非常适合“不为你不需要付出代价”的理念。分配函数本身不需要检查,因为§18.6.1.3,1明确地这样说。

为了解决问题,可以考虑添加第二个重载

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

可悲的是,向委员会提出此建议不太可能导致更改,因为它会破坏现有代码,依赖于使用空指针放置新的确定。