Skip to content

Latest commit

 

History

History
79 lines (56 loc) · 5.17 KB

array_placement_new.md

File metadata and controls

79 lines (56 loc) · 5.17 KB

placement new для массивов

Вам посчастливилось добыть новую суперэффективную библиотеку для управления памятью? Вы хотите пользоваться ею в C++ и не сталкиваться с надуманным UB из-за проблем с лайфтаймами?

Вам повезло! Просто выделяйте память своей библиотекой, создавайте в выделенном буфере объекты с помощью placement new и забот не знайте!

    void* buffer = my_external_malloc(sizeof(T), alignof(T));
    auto pobj = new (buffer) T();

Красиво, просто, здорово!

А что если мы захотим выделить память и разместить в ней массив?

Нет ничего проще!

    void* buffer = my_external_malloc(n * sizeof(T), alignof(T));
    auto pobjarr = new (buffer) T[n];

Все, можно идти пить чай. Задача решена. Мы молодцы. Как похорошел C++ с 11-го стандарта!

Но не может же быть все так просто?

Конечно же нет! До C++20 вариант placement new для массивов имеет полное право испоганить вашу память.

Конструкция

new (buffer) T[n];

согласно примерам (§ 8.5.2.4 (15.4)) из стандарта C++17, переводится в

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 восхитительную формулировку изменили.

Теперь же, если конструкция

new (arg1, arg2...) T[n];

соответствует вызову стандартного

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 []. Будет плохо.