Передача нулевого указателя на размещение нового
Размещение по умолчанию new
Оператор объявлен в 18.6 [support.dynamic] ¶1 с ненулевой спецификацией исключения:
void* operator new (std::size_t size, void* ptr) noexcept;
Эта функция не делает ничего, кроме return ptr;
поэтому разумно noexcept
однако в соответствии с 5.3.4 [expr.new] this15 это означает, что компилятор должен проверить, что он не возвращает ноль перед вызовом конструктора объекта:
-15-
[Примечание: если функция распределения не объявлена со спецификацией исключения без отбрасывания (15.4), это указывает на сбой в распределении памяти, выбрасываяstd::bad_alloc
исключение (пункт 15, 18.6.2.1); в противном случае он возвращает ненулевой указатель. Если функция выделения объявлена со спецификацией исключения без отбрасывания, она возвращает ноль, чтобы указать сбой в распределении памяти, и ненулевой указатель в противном случае. -end note] Если функция выделения возвращает ноль, инициализация не должна выполняться, функция освобождения не должна вызываться, а значение нового выражения должно быть нулевым.
Мне кажется, что (специально для размещения new
, не в общем) эта нулевая проверка - неудачный удар по производительности, хотя и небольшой.
Я отлаживал код где размещение new
использовался в очень чувствительном к производительности пути кода, чтобы улучшить генерацию кода компилятора, и проверка на ноль наблюдалась в сборке. Предоставляя размещение для конкретного класса new
Перегрузка, которая объявляется со спецификацией выброса исключения (даже если она не может выбрасывать), условная ветвь была удалена, что также позволило компилятору генерировать меньший код для окружающих встроенных функций. Результат произнесения размещения new
Функция может генерировать, хотя она не может, был значительно лучше кода.
Поэтому мне было интересно, действительно ли для размещения требуется нулевая проверка new
дело. Единственный способ, которым он может вернуть ноль, это если вы передаете ноль. Хотя возможно и, по-видимому, законно, написать:
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();
NB Этот вопрос намеренно помечен как микрооптимизация, я не предлагаю вам перегружать место размещения new
для всех ваших типов, чтобы "улучшить" производительность. Этот эффект был замечен в очень специфическом критическом случае производительности и основан на профилировании и измерении.
Обновление: DR 1748 делает неопределенным поведение использовать нулевой указатель с новым размещением, поэтому компиляторам больше не требуется выполнять проверку.
1 ответ
Хотя я не вижу там большого вопроса, кроме "Кто-нибудь когда-нибудь нуждался в размещении нового, чтобы правильно обрабатывать случай нулевого указателя?" (У меня нет), я думаю, что дело достаточно интересное, чтобы высказать некоторые мысли по этому вопросу.
Я считаю стандарт нарушенным или неполным в отношении размещения новой функции и требований к функциям распределения в целом.
Если вы внимательно посмотрите на процитированный §5.3.4,13, это означает, что каждая функция распределения должна проверяться на возвращаемый нулевой указатель, даже если это не так. noexcept
, Поэтому его следует переписать в
Если функция выделения объявлена со спецификацией исключения без отбрасывания и возвращает ноль, инициализация не должна выполняться, функция освобождения не должна вызываться, а значение нового выражения должно быть нулевым.
Это не повредит действительности функций распределения, генерирующих исключения, так как они должны подчиняться §3.7.4.1:
[...] Если он успешен, он должен вернуть адрес начала блока памяти, длина которого в байтах должна быть как минимум такой же, как запрашиваемый размер. [...] Возвращаемый указатель должен быть соответствующим образом выровнен, чтобы его можно было преобразовать в указатель любого полного типа объекта с фундаментальным требованием выравнивания (3.11), а затем использовать для доступа к объекту или массиву в выделенном хранилище (до тех пор, пока хранилище явно освобождается путем вызова соответствующей функции освобождения).
И §5.3.4,14:
[Примечание: когда функция выделения возвращает значение, отличное от нуля, это должен быть указатель на блок хранения, в котором зарезервировано пространство для объекта. Предполагается, что блок хранения выровнен соответствующим образом и имеет запрошенный размер. [...] Конец примечания]
Очевидно, что новое место размещения, которое просто возвращает заданный указатель, не может разумно проверить размер доступного хранилища и выравнивание. Следовательно,
§18.6.1.3,1 о размещении новых говорит
[...] Положения (3.7.4) не применяются к этим формам зарезервированного размещения оператора new и оператора delete.
(Я предполагаю, что они пропустили упоминание §5.3.4,14 в этом месте.)
Тем не менее, вместе эти параграфы говорят косвенно: "если вы передадите указатель мусора на функции palcement, вы получите UB, потому что §5.3.4,14 нарушается". Так что это зависит от вас, чтобы проверить работоспособность любого poitner, данного для размещения новых.
В этом духе и с переписанным §5.3.4,13 стандарт может лишить noexcept
от размещения нового, что приводит к добавлению к этому косвенному выводу: "... и если вы передадите ноль, вы также получите UB". С другой стороны, вероятность того, что указатель или указатель со смещением слишком малы, гораздо меньше, чем у нулевого указателя.
Тем не менее, это избавит от необходимости проверки на нуль, и это будет соответствовать философии "не плати за то, что тебе не нужно". Сама функция распределения не должна была бы проверяться, потому что §18.6.1.3,1 прямо говорит об этом.
Чтобы округлить вещи, можно подумать о добавлении второй перегрузки
void* operator new(std::size_t size, void* ptr, const std::nothrow_t&) noexcept;
К сожалению, предложение этого комитету вряд ли приведет к изменению, потому что это нарушит существующий код, полагая, что размещение нового будет в порядке с нулевыми указателями.