C++ 在将原始数据转换为 class 对象时如何处理对齐填充
C++ how to handle alignment padding when casting raw data to class objects
我将文件的大部分作为 "blobs" 数据读取到 char
数组。
我知道这些 blob 的结构,并为不同的结构创建了 classes。
然后我想将读取的 char
数组转换为适当的 class 对象的数组。
这在某些情况下效果很好,但我遇到了 class 成员的对齐/填充是一个问题的情况。
这是一个最小的例子,但我不是从文件中获取数据,而是在 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]
我该如何正确处理?
我可以以某种方式禁用此 padding/alignment (如果这是正确的术语)吗?是否可以为 class 创建一个成员函数来指定如何进行转换?
在 C++20 之前,如果您实际上还没有创建目标类型的对象,则不允许将指针转换为不同的类型并使用它。
从 C++20 开始,这在您的特定情况下是允许的,因为对象将在 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
以确保您不会意外导致代码中已有的不匹配,从而导致越界访问存储阵列。
作为不可移植的替代方案,编译器通常提供属性来强制 class 成员打包而不留任何填充空间,请参阅 this question。但是,这可能会带来显着的性能损失,因为 CPU 通常会假定某些类型的某些对齐方式,如果数据没有像那样对齐,则操作将花费更长的时间,或者可能根本不允许,具体取决于体系结构。
另外,即使你打包你的 Data
结构,我上面关于转换的观点仍然适用,但是它可能允许你只声明
Data data[2];
从头开始,直接从文件中读入这个data
。 (如果 Data
是平凡可复制的,则允许转换 reinterpret_cast<char*>(data)
并通过该指针写入,它在这里,并假设您读取的数据实际上具有 Data
的正确布局。 )
我将文件的大部分作为 "blobs" 数据读取到 char
数组。
我知道这些 blob 的结构,并为不同的结构创建了 classes。
然后我想将读取的 char
数组转换为适当的 class 对象的数组。
这在某些情况下效果很好,但我遇到了 class 成员的对齐/填充是一个问题的情况。
这是一个最小的例子,但我不是从文件中获取数据,而是在 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]
我该如何正确处理? 我可以以某种方式禁用此 padding/alignment (如果这是正确的术语)吗?是否可以为 class 创建一个成员函数来指定如何进行转换?
在 C++20 之前,如果您实际上还没有创建目标类型的对象,则不允许将指针转换为不同的类型并使用它。
从 C++20 开始,这在您的特定情况下是允许的,因为对象将在 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
以确保您不会意外导致代码中已有的不匹配,从而导致越界访问存储阵列。
作为不可移植的替代方案,编译器通常提供属性来强制 class 成员打包而不留任何填充空间,请参阅 this question。但是,这可能会带来显着的性能损失,因为 CPU 通常会假定某些类型的某些对齐方式,如果数据没有像那样对齐,则操作将花费更长的时间,或者可能根本不允许,具体取决于体系结构。
另外,即使你打包你的 Data
结构,我上面关于转换的观点仍然适用,但是它可能允许你只声明
Data data[2];
从头开始,直接从文件中读入这个data
。 (如果 Data
是平凡可复制的,则允许转换 reinterpret_cast<char*>(data)
并通过该指针写入,它在这里,并假设您读取的数据实际上具有 Data
的正确布局。 )