Подтвердить что ты не робот

Передача нулевого указателя на размещение new

Оператор размещения new по умолчанию объявлен в 18.6 [support.dynamic] ¶1 с несинхронизирующей спецификацией исключений:

void* operator new (std::size_t size, void* ptr) noexcept;

Эта функция ничего не делает, кроме 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, а не вообще) эта нулевая проверка является неудачным хитом производительности, хотя и небольшим.

Я отлаживал некоторый код, в котором место размещения new использовалось в кодах с очень высокой степенью производительности, чтобы улучшить генерацию кода компилятора, и проверка на нуль была обнаружена в сборке. Предоставляя перегрузку размещения new для класса, объявленную с помощью спецификации исключения метаданных (даже если она не может быть выбрана), условная ветвь была удалена, что также позволило компилятору генерировать меньший код для окружающих встроенных функций, Результат сказать, что функция размещения new могла бы бросить, даже если она не могла, была значительно лучше кода.

Итак, мне стало интересно, действительно ли нулевая проверка для случая размещения new. Единственный способ вернуть null - это передать его null. Хотя это возможно и, по-видимому, законно, написать:

void* ptr = nullptr;
Obj* obj = new (ptr) Obj();
assert( obj == nullptr );

Я не понимаю, почему это было бы полезно, я предлагаю, было бы лучше, если бы программисту пришлось явно проверить нуль, прежде чем использовать размещение new например.

Obj* obj = ptr ? new (ptr) Obj() : nullptr;

Кто-нибудь когда-либо нуждался в размещении new, чтобы правильно обрабатывать случай с нулевым указателем? (т.е. без добавления явной проверки, что ptr является допустимой ячейкой памяти.)

Мне интересно, было бы разумно запретить передачу нулевого указателя на функцию размещения по умолчанию new, а если нет, есть ли лучший способ избежать ненужной ветки, кроме попытки сказать компилятору, значение не равно нулю, например

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

Или:

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

N.B. Этот вопрос намеренно помечен микро-оптимизацией, я не, предлагая вам перегружать место размещения new для всех ваших типов, чтобы "улучшить" производительность. Этот эффект был замечен в очень специфическом критически важном случае и основан на профилировании и измерении.

Обновление: DR 1748 делает поведение undefined для использования нулевого указателя с новым местом размещения, поэтому компиляторы больше не требуются сделайте чек.

4b9b3361

Ответ 1

Пока я не вижу много вопросов, кроме "Кто-нибудь когда-либо нуждался в размещении нового, чтобы правильно обрабатывать случай с нулевым указателем?" (Я этого не делал), я думаю, что этот случай достаточно интересен, чтобы различить некоторые мысли по этому вопросу.

Я рассматриваю стандартный сломанный или неполный по отношению к новой функции размещения и требованиям к функциям распределения в целом.

Если вы внимательно посмотрите на цитируемый пункт §5.3.4,13, это означает, что каждая функция распределения должна быть проверена для возвращаемого nullpointer, даже если это не noexcept. Поэтому его следует переписать на

Если функция распределения объявлена ​​с не-бросающей спецификацией исключений и возвращает null, инициализация не выполняется, функция освобождения не должна вызываться, а значение нового выражения не имеет значения.

Это не повредит действительности функций распределения, бросающих исключения, поскольку они должны подчиняться §3.7.4.1:

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

И §5.3.4,14:

[Примечание: когда функция распределения возвращает значение, отличное от нуля, оно должно быть указателем на блок хранения, в котором зарезервировано пространство для объекта. Предполагается, что блок хранения будет соответствующим образом выровнен и запрошенного размера. [...] -end note]

Очевидно, что новое место размещения, которое просто возвращает данный указатель, не может разумно проверить размер и выравнивание хранилища. Следовательно,

§18.6.1.3,1 о размещении новых говорит

[...] Положения (3.7.4) не применяются к этим зарезервированным формам размещения оператора new и operator delete.

(я думаю, они упустили упомянуть §5.3.4, 14 в этом месте.)

Однако вместе эти параграфы говорят косвенно: "Если вы передаете указатель мусора на функции подделки, вы получите UB, потому что параграф 5.3.3.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;

К сожалению, предложение этого комитета вряд ли приведет к изменению, поскольку оно сломает существующий код, полагаясь на то, что новое размещение будет нормально с нулевыми указателями.