Почему компилятор C ++ не устраняет нулевую проверку указателя, возвращаемого новым? - PullRequest
8 голосов
/ 21 ноября 2011

Недавно я запустил следующий код на 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 ]

7 голосов
/ 21 ноября 2011

Я думаю, что это очень просто, и вы перепутали две принципиально разные вещи:

  1. malloc () может вернуть все, в частности ноль.

  2. глобальная функция распределения C ++ void * operator new(size_t) throw(std::bad_alloc) требуется стандартом либо для возврата указателя на требуемый объем памяти (+ соответствующим образом выровненный), либо для выхода через исключение.

Если вы хотите заменить функцию глобального распределения, вы несете ответственность за предоставление замены, которая соответствует правилам стандарта. Простейшая версия выглядит так:

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 определено таким образом? Рассмотрим стандартную последовательность распределения, когда вы говорите T * p = ::new T();. Это эквивалентно этому:

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

Если вторая строка выбрасывает (т.е. конструирование завершается неудачно), память освобождается с помощью соответствующей функции освобождения. Однако, если первый вызов заканчивается неудачей, выполнение никогда не должно доходить до второй строки! Единственный способ добиться этого - выйти через исключение. (Версии без выделения функций распределения предназначены только для ручного использования, когда пользовательский код может проверить результат работы распределителя, прежде чем приступить к построению.)

6 голосов
/ 21 ноября 2011

C ++ 11 ясен в вопросе:

void* operator new(std::size_t size);: ... 3 Обязательное поведение: Возвращать ненулевой указатель в соответствующим образом выровненное хранилище (3.7.4), либо выведите исключение bad_alloc. Это требование является обязательным для замещающей версии этой функции.

Вы нажали Неопределенное поведение.

[править] Теперь, почему это препятствует оптимизации?Поставщики компиляторов склонны тратить свое время на придумывание оптимизаций для часто используемых шаблонов кода.Для них обычно мало пользы в оптимизации для более быстрого неопределенного поведения.(Некоторые UB могут быть четко определены в этом конкретном компиляторе и все же оптимизированы, но приведенный выше пример, скорее всего, не будет).

5 голосов
/ 21 ноября 2011

Я думаю, вы ожидаете слишком много от оптимизатора. К тому времени, когда оптимизатор получает этот код, он считает new char просто очередным вызовом функции, возвращаемое значение которого сохраняется в стеке. Таким образом, состояние if не рассматривается как заслуживающее особого отношения.

Вероятно, это вызвано тем, что вы переопределили operator new, и за пределами оптимизатора зарплаты вы можете посмотреть там, увидеть, что вы назвали malloc, который может вернуть NULL, и решить, что эта переопределенная версия не возврат NULL. malloc выглядит как просто еще один вызов функции. Кто знает? Возможно, вы тоже ссылаетесь на эту версию.

Есть несколько других примеров переопределенных операторов, меняющих свое поведение в C ++: operator &&, operator || и operator ,. Каждый из них имеет особое поведение, когда не переопределяется, но ведет себя как стандартные операторы, когда переопределяется. Например, operator && будет даже не вычислять его правую сторону вообще, если левая сторона оценивается как false. Однако, если переопределено, обе стороны из operator && вычисляются до , передавая их operator &&; функция короткого замыкания полностью исчезает. (Это сделано для поддержки использования перегрузки операторов для определения мини-языков в C ++; один пример этого см. В библиотеке Boost Spirit.)

3 голосов
/ 21 ноября 2011

Почему компилятор должен это делать?

При непрозрачной реализации new невозможно узнать, является ли реализация правильной или нет. Ваш нестандарт, так что вам повезло, что он все-таки проверил.

2 голосов
/ 21 ноября 2011

Существует более одного operator new; Смотрите здесь . И вы не объявили свой вариант как исключение. Поэтому компилятор не должен делать вывод, что он никогда не возвращает нулевой указатель.

Я не очень хорошо знаю последний стандарт C ++ 11, но я предполагаю, что только стандарт, определенный operator new (единственное исключение), должен возвращать ненулевой указатель, а не какой-либо определенные пользователем.

А в текущей магистрали GCC файл libstdc++-v3/libsupc++/new, похоже, не содержит какого-либо определенного атрибута, сообщающего GCC, что nil никогда не возвращается ... даже если я считаю, что это неопределенное поведение - получить nil с броском new. 1010 *

0 голосов
/ 25 ноября 2011

Я проверил сборку, созданную с помощью g ++ -O3 -S, и стандартная новая gcc (4.4.5) не удаляет if(ptr == 0).Похоже, что gcc не имеет флагов компилятора или атрибутов функций для оптимизации проверок NULL.

Таким образом, похоже, что gcc не поддерживает этот вид оптимизации в настоящее время.

0 голосов
/ 21 ноября 2011

хотя new никогда не должен возвращать нулевой указатель

Не должно быть при нормальной работе. Но как насчет безумной ситуации, когда кто-то отключает память, или она просто умирает, или она просто наполняется?

Почему компилятор не устраняет этот код? Это просто пропущенный оптимизация или есть какая-то другая причина для этого?

Потому что новое может потерпеть неудачу. Если вы используете версию без бросков, она может вернуть NULL (или nulptr в c ++ 11).

...