Я думаю, что следующий код является допустимым способом сделать это (на самом деле это всего лишь небольшой пример идеи):
#include <memory>
void f(float* buffer, std::size_t buffer_size_in_bytes)
{
double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];
// we have started the lifetime of the doubles.
// "d" is a new pointer pointing to the first double object in the array.
// now you can use "d" as a double buffer for your calculations
// you are not allowed to access any object through the "buffer" pointer anymore since the floats are "destroyed"
d[0] = 1.;
// do some work here on/with the doubles...
// conceptually we need to destory the doubles here... but they are trivially destructable
// now we need to start the lifetime of the floats again
new (buffer) float[10];
// here we are unsure about wether we need to update the "buffer" pointer to
// the one returned by the placement new of the floats
// if it is nessessary, we could return the new float pointer or take the input pointer
// by reference and update it directly in the function
}
int main()
{
float* floats = new float[10];
f(floats, sizeof(float) * 10);
return 0;
}
Важно, чтобы вы использовали только указатель, полученный от нового размещения. И важно разместить новые поплавки. Даже если это нерабочая конструкция, вам нужно снова запустить время жизни поплавков.
Забудьте о std::launder
и reinterpret_cast
в комментариях. Размещение новое сделает эту работу за вас.
edit: убедитесь, что у вас правильное выравнивание при создании буфера в main.
Обновление:
Я просто хотел дать обновленную информацию о вещах, которые обсуждались в комментариях.
- Первое, о чем упоминалось, это то, что нам может понадобиться обновить первоначально созданный указатель с плавающей запятой на указатель, возвращаемый поплавками с заменой новых (вопрос заключается в том, можно ли по-прежнему использовать исходный указатель с плавающей запятой для доступа к поплавкам, потому что числа с плавающей запятой теперь являются «новыми» числами с плавающей запятой, полученными с помощью дополнительного нового выражения).
Для этого мы можем либо а) передать указатель с плавающей запятой по ссылке и обновить его, либо б) вернуть из функции новый полученный указатель с плавающей запятой:
a)
void f(float*& buffer, std::size_t buffer_size_in_bytes)
{
double* d = new (buffer)double[buffer_size_in_bytes / sizeof(double)];
// do some work here on/with the doubles...
buffer = new (buffer) float[10];
}
b)
float* f(float* buffer, std::size_t buffer_size_in_bytes)
{
/* same as inital example... */
return new (buffer) float[10];
}
int main()
{
float* floats = new float[10];
floats = f(floats, sizeof(float) * 10);
return 0;
}
Следующим и более важным моментом, о котором следует упомянуть, является то, что Placement-new может иметь накладные расходы памяти. Таким образом, реализации разрешено размещать некоторые метаданные перед возвращаемым массивом. Если это произойдет, то наивный расчет того, сколько двойников уместится в нашей памяти, будет заведомо неверным. Проблема в том, что мы не знаем, сколько байтов потребуется реализации заранее для конкретного вызова. Но это было бы необходимо, чтобы скорректировать количество двойников, которые, как мы знаем, поместятся в оставшееся хранилище. Здесь ( https://stackoverflow.com/a/8721932/3783662 ) - еще один пост SO, в котором Говард Хиннант провел тест фрагмент. Я проверил это с помощью онлайн-компилятора и увидел, что для тривиальных разрушаемых типов (например, double) накладные расходы равны 0. Для более сложных типов (например, std::string) накладные расходы составляют 8 байтов. Но это может варьироваться для вашей платформы/компилятора. Проверьте это заранее с помощью фрагмента от Говарда.
На вопрос, почему нам нужно использовать какое-то новое размещение (либо с помощью new[], либо с одним элементом new): нам разрешено указывать указатели любым способом, которым мы хотим. Но, в конце концов, когда мы обращаемся к значению, нам нужно использовать правильный тип, чтобы не нарушать строгие правила алиасинга. Проще говоря: доступ к объекту разрешен только тогда, когда действительно существует объект типа указателя, живущий в месте, заданном указателем. Так как же оживить объекты? стандарт говорит:
https://timsong-cpp.github.io/cppwp/intro.object# 1а> :
«Объект создается определением, новым выражением, неявным изменением активного члена объединения или созданием временного объекта».
Есть дополнительный сектор, который может показаться интересным:
https://timsong-cpp.github.io/cppwp/basic.life# 1а>:
«Говорят, что объект имеет непустую инициализацию, если он относится к классу или агрегатному типу, и он или один из его подобъектов инициализируется конструктором, отличным от тривиального конструктора по умолчанию. Время жизни объекта типа T начинается, когда:
- получается хранилище с правильным выравниванием и размером для типа T, и
- если объект имеет непустую инициализацию, его инициализация завершена"
Итак, теперь мы можем утверждать, что, поскольку двойники тривиальны, нужно ли нам предпринимать какие-то действия, чтобы оживить тривиальные объекты и изменить настоящие живые объекты? Я говорю да, потому что мы изначально получили хранилище для поплавков, и доступ к хранилищу через двойной указатель нарушил бы строгое сглаживание. Поэтому нам нужно сообщить компилятору, что фактический тип изменился. Весь этот последний пункт 3 обсуждался довольно спорно. Вы можете составить собственное мнение. Теперь вся информация у вас под рукой.
person
phön
schedule
11.07.2018