C ++, как обрабатывать выравнивание при приведении необработанных данных к объектам класса - PullRequest
0 голосов
/ 11 апреля 2020

Я читаю большие разделы файла как «BLOB-объекты» данных в char массивах. Я знаю, как эти капли структурированы, и создали классы для разных структур. Затем я хочу привести массивы read char к массивам соответствующих объектов класса.

Это хорошо работает в определенных случаях, но я дошел до случая, когда выравнивание / заполнение членов класса является проблемой .

Вот минимальный пример, но вместо того, чтобы получать данные из файла, я определяю данные в data_i1, data_d1 и data_i2, а затем приводю их к c_data. c_data представляет данные, считанные из файла, и содержит data_i1, data_d1 и data_i2 дважды.

Если выравнивание не является проблемой, если я приведу c_data к массиву Data , Я должен получить исходные данные в Data[0] и Data[1].

#include <iostream>

class Data {
public:
    int     i1[2];
    double  d1[3];
    int     i2[3];
};


int main()
{
    //Setting some data for the example:
    int     data_i1[2] = {  1,   100};          //2 * 4 =  8 bytes
    double  data_d1[3] = {0.1, 100.2, 200.3 };  //3 * 8 = 24 bytes
    int     data_i2[3] = {  2,   200, 305   };  //3 * 4 = 12 bytes
                                                //total = 44 bytes

    //As arrays the data is 44 bytes, but size of Data is 48 bytes:
    printf("sizeof(data_i1) = %d\n",    sizeof(data_i1));
    printf("sizeof(data_d1) = %d\n",    sizeof(data_d1));
    printf("sizeof(data_i2) = %d\n",    sizeof(data_i1));
    printf("total size      = %d\n\n",  sizeof(data_i1) + sizeof(data_d1) + sizeof(data_i2));
    printf("sizeof(Data)    = %d\n",    sizeof(Data));


    //This can hold the above that of 44 bytes, twice:
    char c_data[88];

    //Copying the data from the arrays to a char array
    //In reality the data is read from a binary file to the char array
    memcpy(c_data +  0, data_i1,  8);
    memcpy(c_data +  8, data_d1, 24);
    memcpy(c_data + 32, data_i2, 12); //c_data contains data_i1, data_d1, data_i2
    memcpy(c_data + 44,  c_data, 44); //c_data contains data_i1, data_d1, data_i2 repeated twice

    //Casting the char array to a Data array:
    Data* data = (Data*)c_data;

    //The first Data object in the Data array gets the correct values:
    Data data1 = data[0];
    //The second Data object gets bad data:
    Data data2 = data[1];

    printf("data1 : [%4d, %4d] [%4.1f, %4.1f, %4.1f] [%4d, %4d, %4d]\n", data1.i1[0], data1.i1[1], data1.d1[0], data1.d1[1], data1.d1[2], data1.i2[0], data1.i2[1], data1.i2[2]);
    printf("data2 : [%4d, %4d] [%4.1f, %4.1f, %4.1f] [%4d, %4d, %4d]\n", data2.i1[0], data2.i1[1], data2.d1[0], data2.d1[1], data2.d1[2], data2.i2[0], data2.i2[1], data2.i2[2]);

    return 0;
}

Вывод кода:

sizeof(data_i1) = 8
sizeof(data_d1) = 24
sizeof(data_i2) = 8
total size      = 44

sizeof(Data)    = 48
data1 : [   1,  100] [ 0.1, 100.2, 200.3] [   2,  200,  305]
data2 : [ 100, -1717986918] [-92559653364574087271962722384372548731666605007261414794985472.0, -0.0,  0.0] [-390597128,  100, -858993460]

Как мне правильно это обработать? Могу ли я как-то отключить этот отступ / выравнивание (если это правильный термин)? Можно ли создать функцию-член для класса, чтобы указать, как выполняется приведение?

1 Ответ

1 голос
/ 11 апреля 2020

До C ++ 20 вам не разрешено просто приводить указатель на другой тип и использовать его, если вы фактически не создали объект целевого типа.

Начиная с C ++ 20 это разрешено в вашем конкретном случае c, потому что объекты будут создаваться неявно в char массивах, когда они начнут свое время жизни, а объект имеет тип неявного времени жизни , который, как это бывает у вашего Data.

Но даже в C ++ 20 у вас нет гарантии, что между членами структуры не будет заполнения, и поэтому небезопасно просто приводить указатель или memcpy всю структуру. Даже если вы убедитесь, что нет проблем с заполнением, вам необходимо дополнительно обеспечить правильное выравнивание массива хранения с помощью alignas:

alignas(alignof(Data)) char c_data[sizeof(Data)*2];

и, возможно, вам также потребуется вызвать std::launder для указателя чтобы он указывал на неявно созданный объект Data:

Data* data = std::launder(reinterpret_cast<Data*>(c_data));

Вместо того, чтобы делать все это, создайте объект типа Data (или его массив) напрямую (это также разрешает выравнивание эмиссии) и memcpy отдельных членов по одному, чтобы избежать проблем с заполнением:

Data data[2];

// Loop through array and `memcpy` each member individually

Кроме того, не используйте явные числовые константы для размеров и смещений. Всегда используйте sizeof для правильных типов, чтобы быть уверенным, что вы случайно не вызовете несоответствие, которое уже есть в вашем коде, что приведет к выходу за пределы массива хранения.


В качестве непереносимой альтернативы компиляторы обычно предлагают атрибуты, чтобы заставить участников класса упаковываться, не покидая места для заполнения, см. этот вопрос . Однако это может привести к значительным потерям производительности, поскольку ЦП обычно предполагают определенное выравнивание определенных типов, и если данные не выровнены, как это, операции либо будут длиться дольше, либо могут вообще не быть разрешены в зависимости от архитектуры.

Кроме того, даже если вы упакуете структуру Data, приведенные выше замечания о приведении по-прежнему применимы, однако это может позволить вам просто объявить

Data data[2];

с самого начала и непосредственно прочитать из файла в это data. (Приведение reinterpret_cast<char*>(data) и запись через этот указатель разрешены, если Data легко копируемо, что здесь и предполагается, что прочитанные вами данные действительно имеют правильную компоновку для Data.)

...