Вам посчастливилось добыть новую суперэффективную библиотеку для управления памятью? Вы хотите пользоваться ею в 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 [].
Будет плохо.