Thursday, May 28, 2009

Another Note on Empty Structs

I mentioned this C++ Standard Violation by MSVC++ in my last post on empty structs:

- There is no padding (other than for alignment) between the last base class and the first class member or vft-pointer(s). *** NOTE: this is an over-aggressive empty-base-optimization that can break the C++ standard.

Let me explain:

The standard on empty struct inheritance specifies that separate structs of the same type should yield unique addresses. In the following code, the two addresses printed should be different in a standards compliant compiler (Intel or GCC). MSVC++ ends up printing the same address twice.

class a { public: void awork() {} };
class b : public a { public: int bdata; };
class c : public a { public: b cdata; };
c test;
a *pca=&(test);
a *pcb=&(test.cdata);
printf("Offset of c::a %d\n",size_t(pca));
printf("Offset of c.b::a %d\n",size_t(pcb));

Friday, May 1, 2009

Empty Struct / Class & Empty Base Rules for MSVC++ and GCC

I now GROK empty class (and empty base optimizations) fully on MSVC and GCC.

----------------------------------------------------------

GCC C++ follows these rules:

- class EmptyBase {}; --> sizeof(EmptyBase) == 1

- Any number of empty-bases will map to 0 in the struct offset as long as all are unique types (including parenting).

- Non empty-base parents are simply in the order declared with only padding for alignment.

- If the first member of a derived class that immediately follows empty-bases does not derive from any of those bases, it is allowed to start at the first properly aligned offset for that member that is greater-than-or-equal-to the empty-base address -- this may be the same address as the empty-bases.

- If the first member of a derived class that immediately follows empty-bases does derive from any of those bases, it will start at the first properly aligned offset for that member that is greater-than the empty-base address -- this is never the same address as the empty-bases.

- Members that are empty-classes take at least one byte of storage in the containing class.

----------------------------------------------------------

MSVC++ follows these rules:

- class EmptyBase {}; --> sizeof(EmptyBase) == 1

- The only way an empty-base class (or class derived from an empty-base) will start at offset 0 (zero) is if it is the first base class.

- A Non-empty-base class will start at the next valid alignment offset for the base class.

- All empty-base classes will appear to have zero effective storage in the derived class and do not affect the current offset unless followed by another empty-base class (or class derived from an empty-base) in which case you should see the following rule.

- An empty-base class (or class derived from an empty-base) that follows an empty-base class (or class derived from an empty-base) will add 1 to the current offset position before padding to the proper alignment for the class.

- There is no padding (other than for alignment) between the last base class and the first class member or vft-pointer(s). *** NOTE: this is an over-aggressive empty-base-optimization that can break the C++ standard.

- Members that are empty-classes take at least one byte of storage in the containing class.

Thursday, April 30, 2009

The C++ Empty Struct Conundrum

Empty structs are treated differently in C++ than in C. Heck, they're treated differently by different C++ compilers :-(

Here's some weirdness with empty structures being treated differently (i.e. different offsets, sizes, binary layout) between C and C++.

struct foo { };
struct foo2 { };

struct bar
{
    foo var1;
    int var2;
};

struct bar2 : foo
{
    int var2;
};

struct bar3 : foo
{
    int a,b;
};

struct bar4 : foo
{
    bar3    var3;
};

struct bar5 : foo, foo2
{
    int    d;
};

struct bar6 : bar2, foo2
{
    int    d;
};

struct bar7 : foo2
{
    bar2    v;
    int        d;
};

C89
    sizeof(foo) == 0
    sizeof(bar) == 4
    rest are C++-only (inheritance)

C99
    empty "struct foo" not allowed
    sizeof(bar) == 4
    rest are C++-only (inheritance)

C++ Standard (comments show where the C++
        standard requires padding / wasted space)
    sizeof(foo) == 1 (* typical value)
        -- sizeof(foo) != 0   (usually 1 but 
        possibly some other value that is
        compiler/alignment dependent)
    sizeof(bar) == 8
        !!! Wasting space on empty struct !!!
    C++-only:
    sizeof(bar2) == 4
    sizeof(bar3) == 8
        OK - no padding before first member
    sizeof(bar4) == 12
        !!! pad before first member due to
        identical-empty-base conflict between
        base class and first member
    sizeof(bar5) == 4
    sizeof(bar6) == 8
    sizeof(bar7) == 8

GCC C++ (PS3): -- Fully compliant to Standards
        with best-allowed empty-base-optimization
    sizeof(foo)  == 1
    sizeof(foo2) == 1
    sizeof(bar)  == 8
    sizeof(bar2) == 4
    sizeof(bar3) == 8
    sizeof(bar4) == 12
    sizeof(bar5) == 4
    sizeof(bar6) == 8
    sizeof(bar7) == 8

MSVC++
    sizeof(foo)  == 1
    sizeof(foo2) == 1
    sizeof(bar)  == 8
    sizeof(bar2) == 4
    sizeof(bar3) == 8
    sizeof(bar4) == 8
        !!! NONCOMPLIANCE !!! -- sizeof() should be 12
         - overaggressive empty-base-optimization
        (bar4::foo not allowed to resolve to
        same address as bar4.var3::foo) 
    sizeof(bar5) == 8
        Wasted Space - standard allows
        empty-base-optimization of foo & foo2
    sizeof(bar6) == 12
        Wasted Space - standard allows
        empty-base-optimization of foo & foo2
    sizeof(bar7) == 8



-------------------



struct NullCounterNoInit
{
    FORCEINLINE void Inc()            { }
};


struct ThreadOwnedCounter
{
    UINT32    m_nCountVal;
    FORCEINLINE UINT32 Inc() { return(++m_nCountVal); }
};


template 
class Test1
{
    TCounter    counter;
    int            foo;
    int ReadFoo() { counter.Inc(); return(foo); }
}

template 
class Test2 : TCounter
{
    int        foo;
    int ReadFoo() { TCounter::Inc(); return(foo); }
}

sizeof(Test1) == 8
    !!! Wasting space on empty struct !!!
sizeof(Test2) == 4

sizeof(Test1) == 8
sizeof(Test2) == 8