有没有一种聪明的方法可以避免在 C++ 中使用嵌套 类 进行额外填充?

Is there a clever way of avoiding extra padding with nested classes in C++?

这些结构 align1align2 包含相同的数据,但 align1 由于嵌套布局而具有更多填充。 如何获得 align2 的内存节省对齐方式,同时还使用 align1 中的嵌套结构?

int main() {
    struct align1 {
        struct {
            double d;    // 8 bytes
            bool b1;    //+1 byte (+ 7 bytes padding) = 16 bytes
        } subStruct;
        bool b2;        //+1 byte (+ 7 bytes padding) = 24 bytes
    };
    struct align2 {
        double d;        // 8 bytes
        bool b1, b2;    //+2 byte (+ 6 bytes padding) = 16 bytes
    };

    std::cout << "align1: " << sizeof(align1) << " bytes\n";    // 24 bytes
    std::cout << "align2: " << sizeof(align2) << " bytes\n";    // 16 bytes

    return 0;
}

嵌套的 subStruct 结构是必需的,因为它将在 declared/defined 之外。我正在使用 C++17Visual Studio 2017.

生成的代码可能非常脏或难看。我只是不希望它在以后向我抛出随机错误或在更改配置时中断。

我明确依赖于提出“肮脏或难看”的代码的许可……任何东西。更明确地说,我只是提供一个想法。你需要考验自己,自己承担责任。我认为这个问题明确允许未经测试的代码。

使用此代码:

typedef union
{
    struct
    {
        double d;   // 8 bytes
        bool b1;    //+1 byte (+ 7 bytes padding) = 16 bytes
    } nested;
    struct
    {
        double d;       // 8 bytes
        bool b1, b2;    //+2 byte (+ 6 bytes padding) = 16 bytes
    } packed;
} t_both;

我希望得到以下结果 attributes/features:

  • 包含可能在别处定义类型的子结构(可以从包含的头文件中使用)
  • substruct 可访问为 XXX.nested.dXXX.nested.b1
  • 地址与 XXX.packed
  • 相同
  • 访问 XXX.packed.b2nested
  • 中被认为是填充的内容
  • 两个子结构的总大小相同,我希望这意味着即使制作这样的数组也可以

无论您对此做什么,它都可能与以下要求冲突:在写入和读取联合时,所有读取访问必须与最近的写入访问联合的同一部分。因此,写一个读另一个是不被严格允许的。这就是我认为对该代码提案不清楚的地方。也就是说,我经常在相应构造已明确测试的环境中使用这种联合。

为了说明这里是一个功能相同但同样不干净的版本,它更好地说明了子结构可以在别处定义:


/* Inside an included header "whatever.h" : */
typedef struct
{
    double d;   // 8 bytes
    bool b1;    //+1 byte (+ 7 bytes padding) = 16 bytes
} t_ExternDefedStruct;
/* Content of including file */

#include "whatever.h"

typedef union
{
    t_ExternDefedStruct nested;
    struct
    {
        double d;       // 8 bytes
        bool b1, b2;    //+2 byte (+ 6 bytes padding) = 16 bytes
    } packed;
} t_both;

当你的问题是填充时,你的答案是#pragma pack

#pragma pack effect

它适用于 MSVC (where it was invented) and also on GCC(添加它是为了与 MSVC 代码库兼容)。

请注意,弄乱对齐可能会非常糟糕。将 multi-byte 成员放在奇数(对他们而言)字节上将导致 run-time 减速。也就是说,在一个很好的情况下,当您的 CPU 完全支持未对齐的操作时。在糟糕的情况下,它会完全崩溃(AFAIK 试图将它们提供给 SSE 指令或非 x86、RISC CPUs)。

我个人知道 #pragma pack(1) 的唯一合法用途是将二进制文件直接映射到结构,尤其是 headers 位图格式,例如 BMP(来自 wingdi.h 的 BITMAPINFOHEADER)或 TGA。另一个是非常大的数据结构,比如@Arty 提到的 gigabyte-sized 数组。

从更大的角度来看,padding 是一个 time-memory trade-off。在绝大多数情况下,访问对齐良好的变量所节省的 CPU 时间非常值得浪费字节。你需要一个很好的理由来改变它,因为如果不认真分析这两种方法,你就不可能取得成功。

使用 #pragma pack(push, 1) 和一些手动填充,您可以使它们相同。

#include <iostream>

int main() {
#pragma pack(push, 1)
    struct align1 {
        struct {
            double d;   // 8 bytes
            bool b1;    //+1 byte (+ 0 bytes padding) = 9 bytes
        } subStruct;
        bool b2;        //+1 byte (+ 0 bytes padding) = 10 bytes
        char pad_[6];   //+6 bytes (+ 0 bytes padding) = 16 bytes 
    };
#pragma pack(pop)
    struct align2 {
        double d;       // 8 bytes
        bool b1, b2;    //+2 byte (+ 6 bytes padding) = 16 bytes
    };

    std::cout << "align1: " << sizeof(align1) << " bytes\n";    // 16 bytes
    std::cout << "align2: " << sizeof(align2) << " bytes\n";    // 16 bytes

    return 0;
}

输出:

align1: 16 bytes
align2: 16 bytes

我在所有流行的编译器中实现了以下用于打包结构的通用宏:

#if defined(_MSC_VER)
    #define ATTR_PACKED
    #define PACKED_BEGIN __pragma(pack(push, 1))
    #define PACKED_END __pragma(pack(pop))
#else
    #define ATTR_PACKED __attribute__((packed))
    #define PACKED_BEGIN
    #define PACKED_END
#endif

在外部结构之前放置PACKED_BEGIN行,在外部结构之后放置PACKED_END行,在所有结构(包括内)。所有这些标记的结构将被所有流行的编译器以相同的方式密集打包,打包成尽可能小的尺寸。请参阅下面的代码;您的两个结构都将以相同的方式对齐,大小均为 10 bytes

已在在线 C++ 编译器上进行测试(click-open 以下链接可查看测试):MSVC, GCC, and CLang.

如果它被打包得太密集,你可以在需要的地方添加额外的填充字段,在字段之间或字段之后,像 char pad0[2];char pad1[3]; 这样的字段来插入 23 额外的填充字节。

#if defined(_MSC_VER)
    #define ATTR_PACKED
    #define PACKED_BEGIN __pragma(pack(push, 1))
    #define PACKED_END __pragma(pack(pop))
#else
    #define ATTR_PACKED __attribute__((packed))
    #define PACKED_BEGIN
    #define PACKED_END
#endif

#include <iostream>

int main() {
    PACKED_BEGIN
    struct ATTR_PACKED align1 {
        struct ATTR_PACKED {
            double d;
            bool b1;
        } subStruct;
        bool b2;
    };
    PACKED_END

    PACKED_BEGIN
    struct ATTR_PACKED align2 {
        double d;
        bool b1, b2;
    };
    PACKED_END

    std::cout << "align1: " << sizeof(align1) << " bytes\n";
    std::cout << "align2: " << sizeof(align2) << " bytes\n";

    return 0;
}

输出:

align1: 10 bytes
align2: 10 bytes

C++ 11引入了可以使用的关键字“alignas”,这里有一个link(https://en.cppreference.com/w/cpp/language/alignas)