就编译器优化而言,将堆分配更改为堆栈分配是否合法和/或可能?还是会破坏 as-if rule?
例如,假设这是代码的原始版本
{
Foo* f = new Foo();
f->do_something();
delete f;
}
编译器是否能够将其更改为以下内容
{
Foo f{};
f.do_something();
}
我不这么认为,因为如果原始版本依赖于自定义分配器之类的东西,那将会产生影响。标准对此有具体说明吗?
是的,这是合法的。 C++14 的 expr.new/10
:
允许实现省略对可替换全局分配函数(18.6.1.1、18.6.1.2)的调用。当它这样做时,存储由实现提供或通过扩展另一个新表达式的分配来提供。
expr.delete/7
:
如果 delete-expression 的操作数的值不是空指针值,则: — 如果对要删除的对象的 new-expression 的分配调用没有省略并且分配没有扩展 (5.3.4) ,删除表达式应调用一个释放函数(3.7.4.2)。从 new-expression 的分配调用返回的值应作为第一个参数传递给释放函数。 — 否则,如果分配是扩展的,或者是通过扩展另一个 new 表达式的分配来提供的,并且由具有扩展的 new 表达式提供的存储的 new 表达式产生的每个其他指针值的删除表达式已被评估,删除表达式应调用释放函数。扩展新表达式的分配调用返回的值应作为第一个参数传递给释放函数。 — 否则,删除表达式将不会调用释放函数 (3.7.4.2)。
因此,总而言之,将 new
和 delete
替换为定义的实现是合法的,例如使用堆栈而不是堆。
注意:正如 Massimiliano Janes 评论的那样,如果 do_something
抛出,编译器无法完全坚持您的示例的这种转换:在这种情况下,编译器应该省略 f
的析构函数调用(而您的转换后的示例确实在此调用了析构函数案子)。但除此之外,将 f
放入堆栈是自由的。
这些是不等价的。 f.do_something()
可能会抛出,在这种情况下,第一个对象保留在内存中,第二个对象被破坏。
noexcept
对 gcc 和 clang 的优化器没有帮助,但显示函数体有帮助。这可能还有更多。
Foo f{}
。这是有正当理由的,编译器无法知道,例如,它们可能在 valgrind 下运行并想要跟踪所有堆使用情况,或者它们正在调试堆碎片问题。编译器必须在允许的优化和编码人员真正想要的之间取得平衡。编译器应该是朋友,而不是敌人
我想指出 IMO 在其他答案中强调的不够:
struct Foo {
static void * operator new(std::size_t count) {
std::cout << "Hey ho!" << std::endl;
return ::operator new(count);
}
};
分配 new Foo()
通常不能被替换,因为:
允许实现省略对可替换全局分配函数(18.6.1.1、18.6.1.2)的调用。当它这样做时,存储由实现提供或通过扩展另一个新表达式的分配来提供。
因此,与上面的 Foo
示例一样,需要调用 Foo::operator new
。省略这个调用会改变程序的可观察行为。
实际示例:Foo
可能需要驻留在某些特殊的内存区域(如内存映射 IO)中才能正常运行。
new
,分配是否也可以在堆栈上。如果我理解正确,它将永远是动态内存,永远不会在堆栈上。给定的段落说分配的大小可以扩展到更大的动态内存块,或者使用以前的动态内存扩展。