Повторное использование буфера с плавающей точкой для удвоений без неопределенного поведения

В одной конкретной функции 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: Убедитесь, что вы создали правильное выравнивание при создании буфера в главном.

Обновить:

Я просто хотел дать обновленную информацию о вещах, которые обсуждались в комментариях.

  1. Первое, что было упомянуто, это то, что нам может потребоваться обновить первоначально созданный указатель 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;
}
  1. Следующее и более важное замечание заключается в том, что для размещения-new разрешено иметь накладные расходы памяти. Таким образом, для реализации разрешено размещать некоторые метаданные infront возвращаемого массива. Если это произойдет, наивный расчет того, сколько удвоений будет вписываться в нашу память, будет явно ошибочным. Проблема в том, что мы не знаем, сколько байтов будет реализовано заранее для конкретного вызова. Но это было бы необходимо, чтобы скорректировать количество удвоений, которые мы знаем, поместится в оставшееся хранилище. Здесь (fooobar.com/questions/21280/...) является еще одним сообщением SO, где Говард Хиннант предоставил тестовый фрагмент. Я проверил это с помощью онлайн-компилятора и увидел, что для тривиальных разрушаемых типов (например, удваивается) накладные расходы равны 0. Для более сложных типов (например, std :: string) были накладные расходы в 8 байт. Но это может быть изменено для вашего plattform/compiler. Протестируйте его заранее с помощью фрагмента Говарда.

  2. На вопрос, почему нам нужно использовать какое-то место размещения 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

Демо-версия.

Написано на мой планшет, тяжелая работа!