Files
ubbook/pointer_provenance/array_placement_new.md
2024-09-15 18:01:28 +01:00

80 lines
5.2 KiB
Markdown

# placement new для массивов
Вам посчастливилось добыть новую суперэффективную библиотеку для управления памятью?
Вы хотите пользоваться ею в C++ и не сталкиваться с надуманным UB из-за проблем с лайфтаймами?
Вам повезло! Просто выделяйте память своей библиотекой, создавайте
в выделенном буфере объекты с помощью placement new и забот не знайте!
```C++
void* buffer = my_external_malloc(sizeof(T), alignof(T));
auto pobj = new (buffer) T();
```
Красиво, просто, здорово!
А что если мы захотим выделить память и разместить в ней массив?
Нет ничего проще!
```C++
void* buffer = my_external_malloc(n * sizeof(T), alignof(T));
auto pobjarr = new (buffer) T[n];
```
Все, можно идти пить чай. Задача решена. Мы молодцы. Как похорошел C++ с 11-го стандарта!
Но не может же быть все так просто?
Конечно же нет! До C++20 вариант placement new для массивов имеет полное право испоганить вашу память.
Конструкция
```C++
new (buffer) T[n];
```
согласно примерам (§ 8.5.2.4 (15.4)) из стандарта [C++17](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4713.pdf),
переводится в
```C++
operator new[](sizeof(T) * n + x, buffer);
// или operator new[](sizeof(T) * n + x, std::align_val_t(alignof(T)), buffer);
```
Где `x` — никак не специфицируемое неотрицательное число, предназначенное, например, чтобы застолбить место под какую-либо
метаинформацию о выделенном массиве: засунуть число элементов в начало области памяти или расставить маркеры начала/конца или еще что-нибудь, что обычно делают аллокаторы.
То есть placement new для массива вполне может полезть за пределы предоставленного вами буфера. Очень удобно!
В C++20 восхитительную [формулировку](https://eel.is/c++draft/expr.new#19.4) изменили.
Теперь же, если конструкция
```C++
new (arg1, arg2...) T[n];
```
соответствует вызову стандартного
```C++
void* operator new[]( std::size_t count, void* ptr);
```
То все будет хорошо. Никаких магических сдвигов на `+x` не возникнет.
Но если же какой-то доброжелатель определил свой собственный operator placement new... Впрочем, это уже совсем другая история...
--------------
Я не встречал ни одного компилятора, и ни одной поставки стандартной библиотеки, в которых стандартный placement new как-либо двигал указатель на пользовательский буфер.
Реальную угрозу трудноотлавливаемого UB в большей степени представляют user-defined версии placement new.
Чтобы обезопасить себя и вызвать настоящий стандартный placement new, нужно использовать
`::new` и кастить указатель на буфер к `void*`.
Либо положиться на алгоритмы `std::uninitialized_default_construct_n` и подобные ему.
Также нужно отметить, что в C++ нет placement delete синтаксиса.
Мы можем только явно вызвать `operator delete[](void* ptr, void* place)`, стандартная версия которого ничего не делает.
Тут, конечно, нужно понимать разницу между самим `operator delete` и синтаксическими конструкциями
`delete p` и `delete [] p`. Первый занимается только управлением памятью. Последние же — еще и вызывают деструкторы.
В C++ нет именно отдельной синтаксической конструкции, чтобы махом вызывать деструкторы элементов массива, созданного с помощью placement new. Это нужно делать вручную или использовать алгоритм `std::destroy`.
Ни в коем случае не стоит использовать `delete []` против указателя, полученного с помощью placement new [].
Будет [плохо](https://godbolt.org/z/WeWPseKoG).