为什么C++编译器不会消除new返回的指针的空检查?

sha*_*oth 8 c++ compiler-construction new-operator compiler-optimization

最近我在ideone.com上运行了以下代码(gcc-4.3.4)

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <new>

using namespace std;

void* operator new( size_t size ) throw(std::bad_alloc)
{
     void* ptr = malloc( 2 * 1024 * 1024 * 1024);
     printf( "%p\n", ptr );
     return ptr;
}

void operator delete( void* ptr )
{
    free( ptr );
}

int main()
{
    char* ptr = new char;
    if( ptr == 0 ) {
        printf( "unreachable\n" );
    }
    delete ptr;
}
Run Code Online (Sandbox Code Playgroud)

得到这个输出:

(nil)
unreachable
Run Code Online (Sandbox Code Playgroud)

虽然new永远不应该返回一个空指针,因此调用者可以依赖它,编译器可以消除ptr == 0检查并将相关代码视为无法访问.

为什么编译器不会删除该代码?它只是一个错过的优化还是有其他原因?

Ker*_* SB 7

我认为这很简单,你有两个根本不同的东西混淆:

  1. malloc()可以返回任何内容,特别是零.

  2. void * operator new(size_t) throw(std::bad_alloc)标准要求全局C++分配函数返回指向所需存储量(+适当对齐)的指针,或以其他方式退出异常.

如果要替换全局分配功能,则您有责任提供符合标准规则的替换.最简单的版本如下所示:

void * operator new(size_t n) throw(std::bad_alloc) {
  void * const p = std::malloc(n);
  if (p == NULL) throw std::bad_alloc();
  return p;
}
Run Code Online (Sandbox Code Playgroud)

任何严肃的实现实际上应该包含一个循环来调用已注册的new-handler直到分配成功,并且只有在没有新的处理程序时才抛出.

你写的程序很简单.


题外话:为什么这样new定义?你说的时候考虑标准的分配顺序T * p = ::new T();.它相当于:

void * addr = ::operator new(sizeof(T));  // allocation
T * p = ::new (addr) T();                 // construction
Run Code Online (Sandbox Code Playgroud)

如果第二行抛出(即构造失败),则使用相应的释放函数释放存储器.但是,如果第一次调用失败,那么执行必须永远不会到达第二行!实现这一目标的唯一方法是通过异常退出.(分配函数的无抛出版本仅供手动使用,其中用户代码可以在继续构造之前检查分配器的结果.)


MSa*_*ers 6

C++ 11在这个问题上很明确:

void* operator new(std::size_t size);:... 3必需行为:将非空指针返回到适当对齐的存储(3.7.4),否则抛出bad_alloc异常.此要求绑定在此函数的替换版本上.

你点击未定义的行为.

[编辑]现在,为什么这会妨碍优化?编译器供应商倾向于花时间梦想对常用的代码模式进行优化.对于他们来说,优化更快的未定义行为通常没什么好处.(某些UB可能在该特定编译器上定义良好并且仍然被优化,但上面的示例可能不会).


Mik*_*one 5

我认为你期待的优化器太多了.当优化器到达此代码时,它认为new char只是另一个函数调用,其返回值存储在堆栈中.所以它并不认为这种if情况值得特别对待.

这可能是因为你覆盖了这个事实operator new,并且它超出了优化器的工资等级来查看那里,看到你调用malloc,它可以返回NULL,并决定这个被覆盖的版本不会返回NULL.malloc看起来像Just Another Function Call.谁知道?您也可以链接自己的版本.

有覆盖运营商的改变C++的行为其他几个例子:operator &&,operator ||,和operator ,.当不被覆盖时,每个都有一个特殊的行为,但在被覆盖时表现得像标准运算符.例如,如果左侧评估为,operator &&根本不会计算其右侧false.但是,如果重写,双方operator &&计算之前经过他们operator &&; 短路功能完全消失.(这样做是为了支持使用运算符重载来定义C++中的迷你语言;有关此示例,请参阅Boost Spirit库.)