将空指针传递给placement new

Jon*_*ely 42 c++ placement-new micro-optimization noexcept

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

void* operator new (std::size_t size, void* ptr) noexcept;
Run Code Online (Sandbox Code Playgroud)

这个函数没有任何作用,除非return ptr;它是合理的noexcept,但是根据5.3.4 [expr.new]15这意味着编译器必须检查它在调用对象的构造函数之前不返回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 );
Run Code Online (Sandbox Code Playgroud)

我不明白为什么这将是有益的,我认为它会更好,如果程序员有明确使用放置前检查null new

Obj* obj = ptr ? new (ptr) Obj() : nullptr;
Run Code Online (Sandbox Code Playgroud)

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

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

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

要么:

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

注意:这个问题是故意标记的微优化,我并不是说你要new为所有类型的重叠放置来"改善"性能.在非常具体的性能关键情况下,基于分析和测量,注意到了这种效应.

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

Arn*_*rtz 13

虽然我在那里看不到太多问题,但"有没有人需要放置新的来正确处理空指针的情况?" (我没有),我认为这个案子足够有趣,可以解决这个问题.

我认为标准是破坏的或不完整的,一般来说,放置新的功能和要求对分配功能.

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

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

这不会损害抛出异常的分配函数的有效性,因为它们必须遵守§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;
Run Code Online (Sandbox Code Playgroud)

遗憾的是,向委员会提出此建议不太可能导致更改,因为它会破坏现有代码,依赖于使用null指针放置new.