Повторное использование буфера с плавающей точкой для удвоений без неопределенного поведения
В одной конкретной функции C++ у меня появляется указатель на большой буфер с плавающей точкой, который я хочу временно использовать, чтобы хранить половину числа удвоений. Есть ли способ использовать этот буфер в качестве пространства скреста для хранения удвоений, что также допускается (т.е. Не неопределенным поведением) стандартом?
В заключение я хотел бы:
void f(float* buffer)
{
double* d = reinterpret_cast<double*>(buffer);
// make use of d
d[i] = 1.;
// done using d as scratch, start filling the buffer
buffer[j] = 1.;
}
Насколько я понимаю, нет простого способа сделать это: если я правильно понимаю, reinterpret_cast<double*>
как это, вызывает неопределенное поведение из-за псевдонимов типов, а использование memcpy
или float/double union
невозможно без копирования данных и выделяя лишнее пространство, которое побеждает цель и в моем случае оказывается дорогостоящим (и использование объединения для пуна не допускается в C++).
Можно предположить, что поплавковый буфер правильно выровнен для использования для удвоения.
Ответы
Ответ 1
Я думаю, что следующий код - это правильный способ сделать это (это на самом деле просто маленький пример этой идеи):
#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: Убедитесь, что вы создали правильное выравнивание при создании буфера в главном.
Обновить:
Я просто хотел дать обновленную информацию о вещах, которые обсуждались в комментариях.
- Первое, что было упомянуто, это то, что нам может потребоваться обновить первоначально созданный указатель float указателю, возвращаемому поплавками re-placement-new'ed (вопрос в том, может ли исходный указатель float использоваться для доступа к поплавкам, поскольку float теперь являются "новыми" поплавками, полученными дополнительным новым выражением).
Для этого мы можем либо a) передать указатель float по ссылке и обновить его, либо b) вернуть новый полученный указатель float из функции:
а)
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];
}
б)
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;
}
-
Следующее и более важное замечание заключается в том, что для размещения-new разрешено иметь накладные расходы памяти. Таким образом, для реализации разрешено размещать некоторые метаданные infront возвращаемого массива. Если это произойдет, наивный расчет того, сколько удвоений будет вписываться в нашу память, будет явно ошибочным. Проблема в том, что мы не знаем, сколько байтов будет реализовано заранее для конкретного вызова. Но это было бы необходимо, чтобы скорректировать количество удвоений, которые мы знаем, поместится в оставшееся хранилище. Здесь (fooobar.com/questions/21280/...) является еще одним сообщением SO, где Говард Хиннант предоставил тестовый фрагмент. Я проверил это с помощью онлайн-компилятора и увидел, что для тривиальных разрушаемых типов (например, удваивается) накладные расходы равны 0. Для более сложных типов (например, std :: string) были накладные расходы в 8 байт. Но это может быть изменено для вашего plattform/compiler. Протестируйте его заранее с помощью фрагмента Говарда.
-
На вопрос, почему нам нужно использовать какое-то место размещения new (либо с помощью нового [], либо с одним элементом нового): нам разрешено указывать указатели так, как мы хотим. Но в конце - когда мы обращаемся к значению - нам нужно использовать правильный тип, чтобы избежать вопиющих строгих правил псевдонимов. Простота разговора: доступ к объекту возможен только при наличии объекта типа указателя, живущего в местоположении, указанном указателем. Итак, как вы приносите предметы к жизни? в стандарте говорится:
https://timsong-cpp.github.io/cppwp/intro.object#1:
"Объект создается определением посредством нового выражения, когда неявно изменяется активный член объединения или когда создается временный объект".
Существует дополнительный сектор, который может показаться интересным:
https://timsong-cpp.github.io/cppwp/basic.life#1:
"Говорят, что объект имеет непустую инициализацию, если он имеет тип класса или агрегата, и он или один из его подобъектов инициализируется конструктором, отличным от тривиального конструктора по умолчанию. Время жизни объекта типа T начинается, когда:
- хранения с надлежащим выравниванием и размером для типа T, и
- если объект имеет непустую инициализацию, его инициализация завершена "
Итак, теперь мы можем утверждать, что, поскольку двойники тривиальны, нужно ли предпринять какие-то действия, чтобы оживить тривиальные объекты и изменить реальные объекты жизни? Я говорю "да", потому что мы сначала получили хранилище для поплавков, и доступ к хранилищу с помощью двойного указателя нарушил бы строгий псевдоним. Поэтому нам нужно сообщить компилятору, что фактический тип изменился. Весь этот последний пункт 3 был довольно спорным. Вы можете составить собственное мнение. Теперь у вас есть вся информация.
Ответ 2
Вы можете достичь этого двумя способами.
Первый:
void set(float *buffer, size_t index, double value) {
memcpy(reinterpret_cast<char*>(buffer)+sizeof(double)*index, &value, sizeof(double));
}
double get(const float *buffer, size_t index) {
double v;
memcpy(&v, reinterpret_cast<const char*>(buffer)+sizeof(double)*index, sizeof(double));
return v;
}
void f(float *buffer) {
// here, use set and get functions
}
Во-вторых: вместо float *
вам нужно выделить "неподходящий" char[]
буфер и использовать новое место для размещения float или double внутри:
template <typename T>
void setType(char *buffer, size_t size) {
for (size_t i=0; i<size/sizeof(T); i++) {
new(buffer+i*sizeof(T)) T;
}
}
// use it like this: setType<float>(buffer, sizeOfBuffer);
Затем используйте этот аксессуар:
template <typename T>
T &get(char *buffer, size_t index) {
return *std::launder(reinterpret_cast<T *>(buffer+index*sizeof(T)));
}
// use it like this: get<float>(buffer, index) = 33.3f;
Третий способ может быть чем-то вроде ответа phön (см. Мои комментарии под этим ответом), к сожалению, я не могу сделать правильное решение из-за этой проблемы.
Ответ 3
tl; dr Не указывайте указатели alias - вообще - если вы не сообщите компилятору, что вы собираетесь в командной строке.
Самый простой способ сделать это - выяснить, какой компилятор отключает строгий псевдоним и использовать его для исходного файла (ов), о котором идет речь.
Потребности должны, а?
Мысль об этом еще немного. Несмотря на все эти вещи о размещении новых, это единственный безопасный способ.
Зачем?
Ну, если у вас есть два указателя разных типов, указывающих на один и тот же адрес, тогда вы используете псевдоним этого адреса, и у вас есть хороший шанс обмануть компилятор. И неважно, как вы назначили значения этим указателям. Компилятор не помнит об этом.
Так что это единственный безопасный способ, и поэтому нам нужен std::pun
.
Ответ 4
Здесь альтернативный подход, который менее страшный.
Ты говоришь,
... плавающий/двойной союз невозможен без... выделения лишнего пространства, которое побеждает цель и оказывается дорогостоящим в моем случае...
Поэтому просто каждый объект объединения содержит два поплавка вместо одного.
static_assert(sizeof(double) == sizeof(float)*2, "Assuming exactly two floats fit in a double.");
union double_or_floats
{
double d;
float f[2];
};
void f(double_or_floats* buffer)
{
// Use buffer of doubles as scratch space.
buffer[0].d = 1.0;
// Done with the scratch space. Start filling the buffer with floats.
buffer[0].f[0] = 1.0f;
buffer[0].f[1] = 2.0f;
}
Конечно, это усложняет индексирование, и код вызова должен быть изменен. Но у него нет накладных расходов, и это более очевидно правильно.
Ответ 5
Эта проблема не может быть решена в портативном C++.
C++ является строгим, когда дело касается сглаживания указателя. Несколько парадоксально это позволяет скомпилировать его на очень многих платформах (например, где, возможно, double
числа хранятся в разных местах для чисел с float
).
Излишне говорить, что если вы стремитесь к переносу кода, вам нужно будет перекодировать то, что у вас есть. Вторая лучшая вещь - быть прагматичной, признавать, что она будет работать на любой настольной системе, с которой я столкнулся; возможно, даже static_assert
в имени/архитектуре компилятора.
Ответ 6
редактировать
Я подумал об этом еще немного, и это не гарантировало безопасность по причинам, которые я добавил к моему первоначальному ответу. Поэтому я оставлю код здесь для справки, но я не рекомендую его использовать.
Вместо этого сделайте то, что я предлагаю выше. Это позор, мне скорее нравится код, который я написал, несмотря на таинственные downvotes (бьет меня чертовски, я думал, что я сделал хорошую работу здесь).
Редактировать 2: (только для полноты, этот пост уже мертв)
Это решение будет работать только для примитивных типов и POD. Это преднамеренно, учитывая объем первоначального вопроса.
Я думал, что отправлю последующий ответ, потому что @phön нашел лучшее другое решение, чем я, и я хотел немного его убрать и бросить некоторые свои идеи.
Обратите внимание: это серьезный пост. Просто потому, что сегодня я чувствую себя немного беззаботно, не значит, что я просто обманываю.
Во-первых, я бы фактически выделил буфер "master", используя malloc()
. Это потому что:
- Это возвращает мне
void *
, которая на самом деле является подходящей здесь. Вы увидите, почему через минуту. - Это немного более эффективно (хотя это детализация).
- Я могу контролировать выравнивание буфера, если мне нужно (для SSE, скажем) с
aligned_alloc
.
На самом деле это не так. Если я хочу управлять им с помощью умного указателя, я всегда могу использовать пользовательский удалён.
Так почему же эта void *
настолько прекрасна? Потому что это мешает мне делать то, что делал Фён в своем посте, т.е. У него возникло соблазн "вернуть" использование буфера в массив с float
и я не думаю, что это мудро.
Лучше, скорее - это, безусловно, более чистое - использовать new
каждом обращении к буфере как массиве Foo
а затем позволить этому указателю выйти из сферы, когда вы закончите с ним. Накладные расходы минимальны, конечно, для типов POD. Фактически, я ожидал бы, что любой достойный компилятор полностью оптимизирует его в этом случае, но я не тестировал это.
Таким образом, вы должны, конечно, обернуть все это в классе, так что сделайте это. Тогда нам не нужен этот пользовательский делетер. Вот оно.
Класс:
#include <cstdlib>
#include <new>
#include <iostream>
class SneakyBuf
{
public:
SneakyBuf (size_t bufsize, size_t alignment = 8) : m_bufsize (bufsize)
{
m_buf = aligned_alloc (alignment, bufsize);
if (m_buf == nullptr)
throw std::bad_alloc ();
std::cout << std::hex << "m_buf is at " << m_buf << "\n\n";
}
~SneakyBuf () { free (m_buf); }
template <class T> T* Cast (size_t& count)
{
count = m_bufsize / sizeof (T);
return new (m_buf) T; // no need for new [] here
}
private:
size_t m_bufsize;
void *m_buf;
};
Программа тестирования:
void do_float_stuff (SneakyBuf& sb)
{
size_t count;
float *f = sb.Cast <float> (count);
std::cout << std::hex << "floats are at " << f << "\n";
std::cout << std::dec << "We have " << count << " floats\n\n";
f [0] = 0;
// ...
}
void do_double_stuff (SneakyBuf& sb)
{
size_t count;
double *d = sb.Cast <double> (count);
std::cout << std::hex << "doubles are at " << d << "\n";
std::cout << std::dec << "We have " << count << " doubles\n";
d [0] = 0;
// ...
}
int main ()
{
SneakyBuf sb (100 * sizeof (double));
do_float_stuff (sb);
do_double_stuff (sb);
}
Выход:
m_buf is at 0x1e56c40
floats are at 0x1e56c40
We have 200 floats
doubles are at 0x1e56c40
We have 100 doubles
Демо-версия.
Написано на мой планшет, тяжелая работа!