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

80 lines
5.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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).