Search code examples
c++inheritancealignmentlanguage-lawyer

Initializing base part of the derived struct / Unexpected packing derived struct fields to alignment gap of the base struct


It was unexpectedly discovered, that clang++7 allows itself to tightly pack fields of the derived struct (struct "B" with field "B::u16_gap" in the example below) to the alignment gap of the base struct (struct "A"). But function "zero_init" in the example expects it's input to be the "A"-object with non-used alignment gap, so it can be overwritten by "memcpy". This function "zero_init", of course, overwrites any value of the "B::u16_gap"-field, when applied to "B"-object.

// compilation: clang++-7 -std=c++17 test.cpp
#include <cstdint>
#include <cstring>
#include <iostream>

struct A {
    std::uint32_t u32 = 0;
    std::uint16_t u16 = 0;
};

void zero_init(A& a) {
    static constexpr A zero = {};
    std::memcpy(&a, &zero, sizeof(A));
};

struct B: public A {
    std::uint16_t u16_gap = 0;
};

static_assert(sizeof(A) == 8);
static_assert(sizeof(B) == 8); // clang++-7 packs additional field "B::u16_gap" to the alignment gap of the A (for g++-7 it is not true)

int main() {
    B b;
    A& a = b;
    b.u16_gap = 123;
    zero_init(a);
    std::cout << b.u16_gap << std::endl; // writes "0" instead of expected "123"
}

Where is the ill-formed part (deviation from "well behavior" rules of the C++17 standard) in this code?


Solution

  • Like your static assert shows, the size of an A is 8 bytes. This means your static "zero" A-Object consists of 8 bytes of zeros. memcpy doesn't know about types, so you copy the plain 8 bytes to the address of a and thus overwrite the u16_gap field, which is stored in those 8 bytes, like your second assert shows.

    If you want to change this, you can just copy 6 bytes and it should work.