C/C++關於結構的緊湊填充的幾條最佳實踐

blowfish發表於2022-06-26

C/C++中結構可以是緊湊結構,也就是編譯器不會自動向結構中插入任何填充位元組,通常這可以用#pragma pack(1)來指定,這是MSVC定義的,但GCC、clang為了相容都支援這個編譯指示。

注意把填充(padding)和對齊(align)區分開來,對齊指的是某個變數或者結構中的成員變數的記憶體地址必須是2的冪的整數倍(比如2的整數倍),否則就是沒有對齊要求。


用#pragma pack(push, 1)來指定結構的填充粒度,存在的問題是容易漏寫與之配對的#pragma pack(pop),這會汙染到所有後續定義的結構。所以是不推薦使用的,除非有工具能自動檢測是否配對。


一、對於GCC、clang,優先使用__attribute__((packed))。這隻對當前結構生效,不會汙染後續定義的結構。


二、對於MSVC,推薦使用宏來保證#pragma pack(push, 1)和#pragma pack(pop)的配對。MSVC專門自定義了一個關鍵字__pragma,可以用在宏裡面。

注意下面那個分號的位置。


#define PACKED( struct_to_pack ) __pragma( pack(push, 1) ) struct_to_pack __pragma( pack(pop) )
  
PACKED(
   struct MyStruct {
       uint8_t   a;
       uint8_t   b;
       uint8_t   c;
       uint16_t  d;
       uint32_t  e;
   }
);


三、對於其他支援C99的編譯器,C99定義了一個關鍵字_Pragma。可以考慮用下面的宏。

注意下面兩個分號的位置。最後一個分號可以不要。


#define PRAGMA(X)        _Pragma(#X)
#define PRAGMA_PACK_PUSH(n) PRAGMA(pack(push,n))
#define PRAGMA_PACK_POP()   PRAGMA(pack(pop))
  
#define PACKED(struct_to_pack)     \
         PRAGMA_PACK_PUSH(1) \
         struct_to_pack     \
         PRAGMA_PACK_POP()
  
PACKED(
   struct MyStruct {
       uint8_t   a;
       uint8_t   b;
       uint8_t   c;
       uint16_t  d;
       uint32_t  e;
   };
);


四、重要的結構(一般是對外介面,比如硬體位元對映、磁碟檔案結構對映、共享記憶體結構、網路二進位制訊息結構等),每個都用static_assert(sizeof(x) == y)保護一下。這樣只要更換了軟硬體平臺、編譯模式(比如32位和64位)等,可能會檢測出老結構無法適配,需要人工重新審視。C11則用_Static_assert。


參考:

https://stackoverflow.com/questions/1537964/visual-c-equivalent-of-gccs-attribute-packed

https://docs.microsoft.com/en-us/cpp/preprocessor/pragma-directives-and-the-pragma-keyword?view=msvc-170

https://stackoverflow.com/questions/45130677/macro-definition-containing-pragma

相關文章