Размещение по умолчанию 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 это означает, что компилятор должен проверить, что он не возвращает null, прежде чем вызывать конструктор объекта:
-15-
[Замечания: если функция распределения не объявлена со спецификацией исключения без отбрасывания (15.4), это указывает на сбой в распределении памяти, выбрасываяstd::bad_alloc
исключение (пункт 15, 18.6.2.1); в противном случае он возвращает ненулевой указатель. Если функция выделения объявлена со спецификацией исключения без отбрасывания, она возвращает ноль, чтобы указать на неудачу в выделении памяти, и ненулевой указатель в противном случае. —Конечная записка] Если функция выделения возвращает ноль, инициализация не должна выполняться, функция освобождения не должна вызываться, а значение нового выражения должно быть нулевым.
Мне кажется, что (специально для размещения 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();
Нотабене Этот вопрос намеренно помечен микро-оптимизации, я не предполагая, что вы обходите место перегрузки new
для всех ваших типов, чтобы «улучшить» производительность. Этот эффект был замечен в очень специфическом критическом случае производительности и основан на профилировании и измерении.
Обновить: DR 1748 делает неопределенным поведение использовать нулевой указатель с новым размещением, поэтому компиляторам больше не требуется выполнять проверку.
Хотя я не вижу там большого вопроса, кроме «Кто-нибудь когда-нибудь нуждался в новом размещении, чтобы правильно обрабатывать случай нулевого указателя?» (У меня нет), я думаю, что дело достаточно интересное, чтобы высказать некоторые мысли по этому вопросу.
Я считаю стандарт нарушенным или неполным в отношении размещения новой функции и требований к функциям распределения в целом.
Если вы внимательно посмотрите на приведенный §5.3.4,13, это означает, что каждый Функция выделения должна быть проверена на возвращаемый нулевой указатель, даже если это не так noexcept
, Поэтому его следует переписать в
Если функция распределения объявляется с не-бросающей спецификацией исключения и возвращает ноль, инициализация не должна выполняться, функция освобождения не должна вызываться, а значение нового выражения должно быть нулевым.
Это не повредит действительности функций распределения, генерирующих исключения, так как они должны подчиняться §3.7.4.1:
[…] Если он успешен, он должен вернуть адрес начала блока памяти, длина которого в байтах должна быть как минимум такой же, как запрашиваемый размер. […] Возвращаемый указатель должен быть соответствующим образом выровнен, чтобы его можно было преобразовать в указатель любого полного типа объекта с фундаментальным требованием выравнивания (3.11), а затем использовать для доступа к объекту или массиву в выделенном хранилище (до тех пор, пока хранилище явно освобождается путем вызова соответствующей функции освобождения).
А также §5.3.4,14:
[Примечание: когда функция выделения возвращает значение, отличное от нуля, это должен быть указатель на блок хранения, в котором зарезервировано пространство для объекта. Предполагается, что блок хранения выровнен соответствующим образом и имеет запрошенный размер. […] Конец примечания]
Очевидно, что новое место размещения, которое просто возвращает данный указатель, не может разумно проверить размер доступного хранилища и выравнивание. Следовательно,
§18.6.1.3,1 о размещении нового говорит
[…] Положения (3.7.4) не применяются к этим зарезервированным формам размещения операторов new и operator delete.
(Я предполагаю, что они пропустили упоминание §5.3.4,14 в этом месте.)
Тем не менее, вместе эти пункты говорят косвенно msgstr «если вы передадите указатель мусора в функции 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;
К сожалению, предложение этого комитету вряд ли приведет к изменению, потому что это нарушит существующий код, полагая, что размещение нового будет в порядке с нулевыми указателями.
Других решений пока нет …