2

I stumbled upon a very bizarre struct member packing in 32-bit clang. Here is the link to the compiler explorer experiment.

In essence, I have the following structs:

struct _8Bytes
{
    uint64_t _8bytes;
};
struct _16Bytes : _8Bytes
{
    uint32_t  _4bytes;
};
struct Test : _16Bytes
{
    uint8_t test;
};

The sizeof(_16Bytes) is 16 as expected, however offsetof(Test, test) is 12 because the compiler decided to pack it right after _16Bytes::_4bytes. This is extremely annoying and I would like to disable this behavior in the first place, however so be it.

What puzzles me is if I change the _16Bytes struct as follows:

struct _16Bytes
{
    // Same as Test1::_16Bytes, but 8 bytes is now a member
    uint64_t _8bytes;
    uint32_t  _4bytes;
};

then all the sudden offsetof(Test, test) is 16. This makes absolutely zero sense to me - can somebody explain what is going on?

More importantly, is there a way to disable this annoying packing behavior?

Egor
  • 779
  • 5
  • 20
  • 3
    Evidently layout for base classes is not the same as for members. This is presumably specified by the ABI; if you could change it globally you would have to rebuild all your libraries. The simplest way to work around would be to include an additional dummy member in `_16Bytes`. – Nate Eldredge Jan 16 '22 at 05:54
  • 1
    Side note: I have never seen inheritance done without specifying the access specifier (`public`, `protected`, or `private`). I had to [find that here](https://stackoverflow.com/a/4797014/4561887). – Gabriel Staples Jan 16 '22 at 05:57
  • The _16Bytes you show doesn't have a test member (Test does but not _16Bytes). The offsetof macro I'm familiar with is offsetof(structName, memberName). – SoronelHaetir Jan 16 '22 at 05:57
  • @NateEldredge Recompiling everything is fine - I need a universal solution. Padding is very error prone as the code is compiled for both 64 and 32 bit and there are complex dependencies between structures. Trying to catch every possible scenario where this may happen is very annoying and error-prone – Egor Jan 16 '22 at 05:57
  • @SoronelHaetir Fixed the typo. I meant offsetof(Test, test). – Egor Jan 16 '22 at 06:03
  • The only gcc/clang options I know of to alter aggregate layout is `-fpack-structs` which does the opposite of what you want. So I suspect you're out of luck unless you want to hack the compiler. – Nate Eldredge Jan 16 '22 at 06:07
  • @NateEldredge I think this compiler behavior is an absolute nonsense. MSVC does not do this and it is unfortunate that clang/gcc do not have an option to disable it. Note that this only happens on 32-bit clang/gcc, but not on 64-bit version. – Egor Jan 16 '22 at 06:12
  • @NateEldredge In both cases I can cast an instance of Test to _16Bytes (either variant), and assign to it. In both cases the size of _16Bytes is 16 bytes, so I don't really understand what difference it makes from the point of view of Test. – Egor Jan 16 '22 at 06:29
  • 1
    This works as specified in the Itanium ABI, which gcc and clang follow. There is no compiler option to do otherwise. If you rely on any particular class layout, your design is probably broken anyway. – n. m. could be an AI Jan 16 '22 at 07:45
  • 1
    Here is a link to the Itanium ABI: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#layout – Sebastian Jan 16 '22 at 08:08
  • 1
    AFAIK the C++ standard says nothing about how compiler should order inherited and base classes with respect to each other. I.e. it's left to the implementation. [read this](http://eel.is/c++draft/class.derived#general-5): _"The order in which the base class subobjects are allocated in the most derived object ([intro.object]) is unspecified."_ I.e. you cannot make assumptions on the memory layout. Edit: but I just see in the comment with the answer, that this might have changed for C++17... – JHBonarius Jan 16 '22 at 08:27

1 Answers1

8

Here's my test program in Compiler Explorer using clang.
And here is the same program in OnlineGDB using gcc.

The sizeof(_16Bytes) is 16 as expected, however offsetof(Test, test) is 12 because the compiler decided to pack it right after _16Bytes::_4bytes.

This actually makes perfect sense to me, based on standard packing rules, assuming that struct Test : _16Bytes turns into the equivalent of this:

struct Test
{
    uint64_t _8bytes;  // inherited from `struct _8Bytes` 
    uint32_t  _4bytes; // inherited from `struct _16Bytes`
    uint8_t test;      // directly part of `struct Test`
};

That's because this struct is naturally packed since it is arranged in order of largest to smallest data type. The uint64_t requires 8-byte alignment, and already has it, the uint32_t requires 4-byte alignment, and already has it, and the uint8_t requires 1-byte alignment, and already has it. Therefore, the only padding required gets added to the very end, like this:

struct Test
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
    // 3 bytes of padding to force 8-byte alignment of the whole struct
};  // struct is 16 bytes total

So, I expect sizeof(_16Bytes) to be 16, and offsetof(Test, test) to be 12.

However, it is a faulty assumption to assume that struct Test : _16Bytes turns into the struct above. Actually, this is undefined behavior apparently when the struct inherits from another class or struct. clang shows this invalid-offsetof warning on Compiler Explorer:

Output of x86-64 clang 13.0.0 (Compiler #1):

<source>:61:44: warning: offset of on non-standard-layout type 'struct Test' [-Winvalid-offsetof]
    printf("offsetof(Test, test) = %lu\n", offsetof(struct Test, test));

...and gcc shows this warning for the same line on OnlineGDB:

main.cpp:61:53: warning: offsetof within non-standard-layout type ‘Test’ is undefined [-Winvalid-offsetof]
     printf("offsetof(Test, test) = %lu\n", offsetof(struct Test, test));

The gcc output makes it more clear: "offsetof within non-standard-layout type ‘Test’ is undefined".

Note: clang, by design, tries to be gcc-compatible. See here: https://clang.llvm.org/ --> End User Features --> "GCC compatibility."

You said:

then all the sudden offsetof(Test, test) is 16. This makes absolutely zero sense to me - can somebody explain what is going on?

Therefore, when you make the change to struct _16Bytes and see that offsetof(Test, test) becomes a really weird and anomalous value of 16, that is also undefined behavior, and therefore has no guaranteed nor predictable behavior we can analyze except for looking at the specifics of the clang compiler, which is pointless, since it is undefined behavior by the standard and could change at any moment anyway. So, you must avoid the undefined behavior I think and not use inheritance if you desire to read an offset.

This is extremely annoying and I would like to disable this behavior in the first place, however so be it.

More importantly, is there a way to disable this annoying packing behavior?

You have to manually pack your structs as you see fit--however you'd like them to be. I'm not sure what you'd like. Do you want the uint8_t to have an offset of 15 instead of 12? If so, do this:

Ex:

struct Test4
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint8_t test;
};  // struct is 16 bytes total

Now, sizeof(Test4) is 16, and offsetof(Test4, test) is 15 instead of 12.

Note that depending on what you are trying to accomplish, you may need to forcefully remove all automatic padding by adding __attribute__ ((__packed__)) just after the word struct and just before the struct name.

Example: this very non-standard padding, in conjunction with the packed attribute, allows struct __attribute__ ((__packed__)) Test5 to have a size of 16 and offsetof(Test5, test) to still be 15:

struct __attribute__ ((__packed__)) Test5
{
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};

Also take a look at the alignas() specifier in C++ to see if it can be used to create your desired effect. And, look into #pragma pack.

Keep in mind you can also achieve certain desired results by including a struct as a member of another struct directly, rather than by using inheritance as you have done. Including one struct in another can avoid the undefined behavior, while doing the inheritance and expecting certain offset results invokes the undefined behavior.

Lastly, you can consider writing Python scripts to autogenerate C++ code for you which generates any necessary struct definitions and handles padding/packing/alignment, and serialization concerns for you. You can define packets in YAML (preferred, in my opinion) or JSON files. This is pretty common practice I think--using Python to autogenerate C or C++ for you. I show how to import yaml files in Python here. However, avoid autogenerating C or C++ using Python if possible, as it may end up creating more code complexity, complicated abstraction, and confusion for fellow developers in an attempt to create less. But, that's for you to decide based on your total situation, use-case, and architecture.

Here is my final and full test code:

https://onlinegdb.com/nfp19v8m3

/******************************************************************************

Welcome to GDB Online.
GDB online is an online compiler and debugger tool for C, C++, Python, Java, PHP, Ruby, Perl,
C#, VB, Swift, Pascal, Fortran, Haskell, Objective-C, Assembly, HTML, CSS, JS, SQLite, Prolog.
Code, Compile, Run and Debug online from anywhere in world.

GS 
15 Jan. 2022 

See: https://stackoverflow.com/questions/70727668/bizarre-struct-member-packing-in-32-bit-clang

*******************************************************************************/
#include <iostream>

struct _8Bytes
{
    uint64_t _8bytes;
};

struct _16Bytes : _8Bytes
{
    uint32_t  _4bytes;
};

struct _16Bytes2
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
};

struct Test : _16Bytes
{
    uint8_t test;
};

struct Test2
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};

struct Test3 : _16Bytes2
{
    uint8_t test;
};

struct Test4
{
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint8_t test;
};

struct __attribute__ ((__packed__)) Test5
{
    uint8_t padding[3]; // explicitly place 3 bytes of padding
    uint64_t _8bytes;
    uint32_t  _4bytes;
    uint8_t test;
};


int main()
{
    printf("sizeof(_8Bytes) = %lu\n", sizeof(_8Bytes));
    printf("sizeof(_16Bytes) = %lu\n", sizeof(_16Bytes));
    printf("sizeof(_16Bytes2) = %lu\n", sizeof(_16Bytes2));
    printf("sizeof(Test) = %lu\n", sizeof(Test));
    printf("sizeof(Test2) = %lu\n", sizeof(Test2));
    printf("sizeof(Test3) = %lu\n", sizeof(Test3));
    printf("sizeof(Test4) = %lu\n", sizeof(Test4));
    printf("sizeof(Test5) = %lu\n", sizeof(Test5));
    printf("\n");
    
    printf("offsetof(Test, test) = %lu\n", offsetof(Test, test));
    printf("offsetof(Test2, test) = %lu\n", offsetof(Test2, test));
    printf("offsetof(Test3, test) = %lu\n", offsetof(Test3, test));
    printf("offsetof(Test4, test) = %lu\n", offsetof(Test4, test));
    printf("offsetof(Test5, test) = %lu\n", offsetof(Test5, test));

    return 0;
}

References:

  1. https://gcc.gnu.org/onlinedocs/gcc/Common-Variable-Attributes.html - official gcc documentation for __attribute__ ((__packed__)), which clang also supports. Search the page for "packed".
  2. Default inheritance access specifier - I have never seen inheritance done without specifying the access specifier (public, protected, or private). I had to find that here.

See also:

  1. https://en.cppreference.com/w/cpp/language/alignof
  2. https://en.cppreference.com/w/cpp/language/alignas
  3. *****What is the difference between "#pragma pack" and "__attribute__((aligned))" - in short, #pragma pack(1) // set packing AND alignment to 1 // place struct definition here #pragma pack() // unset packing AND alignment type syntax is more-restrictive than gcc attribute syntax, and is essentially equivalent to __attribute__((packed,aligned(1))), which is NOT necessarily what you want, since you probably want the struct packed to 1-byte but NOT aligned to 1-byte!
    1. See also: Anybody who writes #pragma pack(1) may as well just wear a sign on their forehead that says “I hate RISC” <-- DON'T BE THAT PERSON! So, just use __attribute__ ((__packed__)) instead of #pragma pack(1)!
Gabriel Staples
  • 36,492
  • 15
  • 194
  • 265
  • 2
    "offsetof within non-standard-layout type ‘Test’ is undefined": That is true only before C++17. After that it is conditionally-supported and if you give the `-std=c++17` flag to gcc it also adjusts its warning message accordingly. Given that it still compiles the program, the use should be supported. – user17732522 Jan 16 '22 at 07:42
  • @user17732522, did you test this? In OnlineGDB I had the "Language" set to "C++ 17" for all tests shown here. I didn't specify that though in clang on Compiler Explorer. – Gabriel Staples Jan 16 '22 at 07:46
  • 1
    https://godbolt.org/z/ad6E6e7aG – user17732522 Jan 16 '22 at 07:47
  • @user17732522, I see. Thanks. – Gabriel Staples Jan 16 '22 at 07:51
  • Thank you for the very details answer! What I am trying to do is to implement something very similar to Microsoft COM headers - have the same API for c and c++. For that I need structs with inheritance to be packed the same way as if they were members of one struct. I have static checks in the code that catch situations with misalignment, but I was wondering if it is possible to globally disable this. I did not realize that I hit the UB here, which explains why this is happening. Thank you! – Egor Jan 16 '22 at 17:35