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

时间:2011-11-21 14:24:52

标签: 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;
}

得到了这个输出:

(nil)
unreachable

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

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

7 个答案:

答案 0 :(得分:7)

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

  1. malloc()可以返回任何内容,尤其是零。

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

    < / LI>

    如果您想要替换全局分配功能,则您的责任提供符合标准规则的替代品。最简单的版本如下:

    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;
    }
    

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

    你写的程序很简单。


    Digression:为什么这个new定义了这种方式?当您说T * p = ::new T();时,请考虑标准分配顺序。它相当于:

    void * addr = ::operator new(sizeof(T));  // allocation
    T * p = ::new (addr) T();                 // construction
    

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

答案 1 :(得分:6)

C ++ 11在这个问题上很清楚:

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

您点击未定义的行为。

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

答案 2 :(得分:5)

我认为你期待的优化器太多了。当优化器获得此代码时,它认为new char只是另一个函数调用,其返回值存储在堆栈中。所以它没有看到if条件值得特别对待。

这可能是因为您覆盖了operator new这一事实,并且超出了优化工具的薪资等级而看到了那里,看到您调用了malloc,可以返回NULL,以及确定此重写版本不会返回NULLmalloc看起来像Just Another Function Call。谁知道?您也可以链接自己的版本。

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

答案 3 :(得分:3)

编译器为什么要这样做?

使用new的不透明实现,无法知道实现是否正确。你的是非标准的,所以你很幸运,它毕竟检查过。

答案 4 :(得分:2)

有多个operator new;见here。并且您没有宣布您的那个可能性抛出异常。所以编译器不应该推断它永远不会返回空指针。

我不太了解最新的C ++ 11标准,但我想它只是标准定义的operator new(一个抛出异常),它应该返回一个非零指针,没有任何用户定义的。

在当前的GCC主干中,文件libstdc++-v3/libsupc++/new似乎没有任何特定的属性告诉GCC nil永远不会返回...即使我相信它是未定义的行为,因为抛出新的nil

答案 5 :(得分:0)

  

虽然new应该永远不会返回空指针

它不应该在正常操作中。但是,当有人插入记忆,或者它只是死亡,或者它刚刚满了时,一个疯狂的情况呢?

  

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

因为新的可能会失败。如果你使用no-throw版本,它可以返回NULL(或c ++ 11中的nulptr)。

答案 6 :(得分:0)

我检查了使用g ++ -O3 -S生成的程序集,标准new,gcc(4.4.5)不会删除if(ptr == 0)。 似乎gcc没有编译器标志或函数属性来优化NULL检查。

所以看来gcc目前不支持这种优化。