Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Packing non-POD type: no warning, unexpected size?

Tags:

c++

Here is a rather contrived series of types. A2 is just a non-POD version of A:

template <size_t N>
struct A { 
    char data[N];
} __attribute__((packed));

template <size_t N>
struct A2 {
    A2() { // actual body not significant
        memset(data, 0, N); 
    }   

    char data[N];
} __attribute__((packed));

template <template <size_t> class T>
struct C { 
    T<9> a;
    int32_t i;
    T<11> t2; 
} __attribute__((packed));
//}; // oops, forgot to pack

template <template <size_t> class T>
struct B : C<T> {
    char footer;
} __attribute__((packed));

As-is, sizeof(B<A>) == sizeof(B<A2>) == 25. I get no warnings compiling with -Wall -pedantic (this is gcc 4.9.0).

But now, let's say I forgot to pack C. Now I get:

sizeof(B<A>) == 32
sizeof(B<A2>) == 28

Still no warnings. What happened here? How is B<A2> smaller than B<A>? Is this just undefined behavior due to A2 not being POD?

If I reorganize B to look have a C<T> member instead of inheriting from it, then and only then do I get a warning:

ignoring packed attribute because of unpacked non-POD field ‘C B::c’

[Update] In response to IdeaHat's comments, here are some other the offsets:

        B<A>  B<A2>
a       0     0
i       12    12
t2      16    16
footer  28    27

And

sizeof(C<A>) == 28
sizeof(C<A2>) == 28

[Update 2] I see the same behavior on clang with respect to the offsets, except that sizeof(B<A>) is 29 instead of 32.

like image 903
Barry Avatar asked Nov 14 '14 16:11

Barry


1 Answers

That behaviour is due to the compiler is allowed to apply optimizations in a non-POD type (e.g.: C<A2>).
It doesn't apply to POD types (e.g.:C<A>).

I've found this related question with a very helpful answer by Kerrek SB:
When extending a padded struct, why can't extra fields be placed in the tail padding?

On the other hand, you can force this optimization regardless the POD'ness with the -fpack-struct option in GCC. Although not recommended, it's useful for the example.

#include <stdint.h>
#include <stdio.h>

struct C {
    int16_t i;
    char t[1];
};

struct C2 {
    C2() {}
    int16_t i;
    char t[1];
};

template <class T>
struct B : T {
    char footer;
};

int main(void) {
    printf("%lu\n", sizeof(B<C>));
    printf("%lu\n", sizeof(B<C2>));
    return 0;
}

If you compile it with -fpack-struct (gcc-4.7):

sizeof(B<C>) == sizeof(B<C2>) == 4

If not:

sizeof(B<C>) == 6
sizeof(B<C2>) == 4

From man gcc (4.7):

-fpack-struct[=n]
       Without a value specified, pack all structure members together
       without holes. 
       When a value is specified (which must be a small power of two),
       pack structure members according to this value, representing
       the maximum alignment (that is, objects with default alignment
       requirements larger than this will be output potentially
       unaligned at the next fitting location.

       Warning: the -fpack-struct switch causes GCC to generate code
       that is not binary compatible with code generated without that
       switch. 
       Additionally, it makes the code suboptimal. Use it to conform
       to a non-default application binary interface.

As you can see, when a class is POD and it acts as the base class of another class, that base is not packed unless you force it. i.e.: it doesn't use the tail padding of the base.


In the particular case of the C++ ABI which GCC uses, there's a discussion about this:

It looks like the ABI document meant to require the reuse of tail-padding in non PODs, but it doesn't actually say that.

Consider this case, as the canonical example:

struct S1 {
    virtual void f();
    int i;
    char c1;
};

struct S2 : public S1 {
    char c2;
};

I think the ABI meant to say that you put "c2" in the tail padding for S1. (That is what G++ implements, FWIW.)


Take a look at what the Itanium ABI C++ (this is the one GCC uses) specifies about tail padding:

This ABI uses the definition of POD only to decide whether to allocate objects in the tail-padding of a base-class subobject. While the standards have broadened the definition of POD over time, they have also forbidden the programmer from directly reading or writing the underlying bytes of a base-class subobject with, say, memcpy. Therefore, even in the most conservative interpretation, implementations may freely allocate objects in the tail padding of any class which would not have been POD in C++98. This ABI is in compliance with that.


In addition, here's the reason why the C++ ABI doesn't use the tail padding of POD objects:

We ignore tail padding for PODs because an early version of the standard did not allow us to use it for anything else and because it sometimes permits faster copying of the type.

In your example, C<A> is POD and for that reason the ABI is not using its tail padding when the type act as the base class of B<A>.
For that, C<A> remains with padding (and occupying 28 bytes as it) and footer occupies 4 bytes respecting the alignment.


Finally, I want to share the code I used to make some test, previously to find a proper answer. You can find it useful in order to see what the compiler ABI does with the objects in C++(11) (GCC).

Test code

#include <iostream>
#include <stddef.h>

struct C {
    int16_t i;
    char t[1];
};

struct C2 {
    C2() {}
    int16_t i;
    char t[1];
};

template <class T>
struct B : T {
    char footer;
};

int main(void) {
    std::cout << std::boolalpha;

    std::cout << "standard_layout:" << std::endl;
    std::cout << "C: " << std::is_standard_layout<C>::value << std::endl;
    std::cout << "C2: " << std::is_standard_layout<C2>::value << std::endl;
    std::cout << "B<C>: " << std::is_standard_layout<B<C>>::value << std::endl;
    std::cout << "B<C2>: " << std::is_standard_layout<B<C2>>::value << std::endl;

    std::cout << std::endl;
    std::cout << "is_trivial:" << std::endl;
    std::cout << "C: " << std::is_trivial<C>::value << std::endl;
    std::cout << "C2: " << std::is_trivial<C2>::value << std::endl;
    std::cout << "B<C>: " << std::is_trivial<B<C>>::value << std::endl;
    std::cout << "B<C2>: " << std::is_trivial<B<C2>>::value << std::endl;

    std::cout << std::endl;
    std::cout << "is_pod:" << std::endl;
    std::cout << "C: " << std::is_pod<C>::value << std::endl;
    std::cout << "C2: " << std::is_pod<C2>::value << std::endl;
    std::cout << "B<C>: " << std::is_pod<B<C>>::value << std::endl;
    std::cout << "B<C2>: " << std::is_pod<B<C2>>::value << std::endl;

    std::cout << std::endl;
    std::cout << "offset:" << std::endl;
    std::cout << "C::i offset " << offsetof(C, i) << std::endl;
    std::cout << "C::t offset " << offsetof(C, t) << std::endl;
    std::cout << "C2::i offset " << offsetof(C2, i) << std::endl;
    std::cout << "C2::t offset " << offsetof(C2, t) << std::endl;
    B<C> bc;
    std::cout << "B<C>.i: " << (int)(reinterpret_cast<char*>(&bc.i) - reinterpret_cast<char*>(&bc)) << std::endl;
    std::cout << "B<C>.t: " << (int)(reinterpret_cast<char*>(&bc.t) - reinterpret_cast<char*>(&bc)) << std::endl;
    std::cout << "B<C>.footer: " << (int)(reinterpret_cast<char*>(&bc.footer) - reinterpret_cast<char*>(&bc)) << std::endl;
    B<C2> bc2;
    std::cout << "B<C2>.i: " << (int)(reinterpret_cast<char*>(&bc2.i) - reinterpret_cast<char*>(&bc2)) << std::endl;
    std::cout << "B<C2>.t: " << (int)(reinterpret_cast<char*>(&bc2.t) - reinterpret_cast<char*>(&bc2)) << std::endl;
    std::cout << "B<C2>.footer: " << (int)(reinterpret_cast<char*>(&bc2.footer) - reinterpret_cast<char*>(&bc2)) << std::endl;

    std::cout << std::endl;
    std::cout << "sizeof:" << std::endl;
    std::cout << "C: " << sizeof(C) << std::endl;
    std::cout << "C2: " << sizeof(C2) << std::endl;
    std::cout << "DIFFERENCE:\n";
    std::cout << "B<C>: " << sizeof(B<C>) << std::endl;
    std::cout << "B<C2>: " << sizeof(B<C2>) << std::endl;
    std::cout << "B<C>::C: " << sizeof(B<C>::C) << std::endl;
    std::cout << "B<C2>::C: " << sizeof(B<C2>::C2) << std::endl;

    std::cout << std::endl;
    std::cout << "alignment:" << std::endl;
    std::cout << "C: " << std::alignment_of<C>::value << std::endl;
    std::cout << "C2: " << std::alignment_of<C2>::value << std::endl;
    std::cout << "B<C>: " << std::alignment_of<B<C>>::value << std::endl;
    std::cout << "B<C2>: " << std::alignment_of<B<C2>>::value << std::endl;
    std::cout << "B<C>::C: " << std::alignment_of<B<C>::C>::value << std::endl;
    std::cout << "B<C2>::C2: " << std::alignment_of<B<C2>::C2>::value << std::endl;
    std::cout << "B<C>.i: " << std::alignment_of<decltype(std::declval<B<C>>().i)>::value << std::endl;
    std::cout << "B<C>.t: " << std::alignment_of<decltype(std::declval<B<C>>().t)>::value << std::endl;
    std::cout << "B<C>.footer: " << std::alignment_of<decltype(std::declval<B<C>>().footer)>::value << std::endl;
    std::cout << "B<C2>.i: " << std::alignment_of<decltype(std::declval<B<C2>>().i)>::value << std::endl;
    std::cout << "B<C2>.t: " << std::alignment_of<decltype(std::declval<B<C2>>().t)>::value << std::endl;
    std::cout << "B<C2>.footer: " << std::alignment_of<decltype(std::declval<B<C2>>().footer)>::value << std::endl;

    return 0;
}

Output with gcc-4.7

standard_layout:
C: true
C2: true
B<C>: false
B<C2>: false

is_trivial:
C: true
C2: false
B<C>: true
B<C2>: false

is_pod:
C: true
C2: false
B<C>: false
B<C2>: false

offset:
C::i offset 0
C::t offset 2
C2::i offset 0
C2::t offset 2
B<C>.i: 0
B<C>.t: 2
B<C>.footer: 4
B<C2>.i: 0
B<C2>.t: 2
B<C2>.footer: 3

sizeof:
C: 4
C2: 4
DIFFERENCE:
B<C>: 6
B<C2>: 4
B<C>::C: 4
B<C2>::C: 4

alignment:
C: 2
C2: 2
B<C>: 2
B<C2>: 2
B<C>::C: 2
B<C2>::C2: 2
B<C>.i: 2
B<C>.t: 1
B<C>.footer: 1
B<C2>.i: 2
B<C2>.t: 1
B<C2>.footer: 1
like image 82
whoan Avatar answered Sep 22 '22 21:09

whoan