Search code examples
c++classpolymorphismvtableobject-layout

sizeof CRTP class implementing an interface


i have a question about the sizeof an instance of a class that uses CRTP to implement an interface, as the following example.

#include <iostream>
class Interface
{
public:
    virtual int foo() {
        std::cout << "Interface\n";
        return 0;
        };
};

template <class Derived>
class Base : Interface
{
public:
    virtual int foo() override {
        Interface::foo();
        std::cout << "base\n";
        return static_cast<Derived*>(this)->bar2;
        }
    int bar;
};

class Derived : public Base<Derived>
{
public:
    virtual int foo() override {
            std::cout << "Derived\n";
        Base<Derived>::foo();
        return bar;
    }
    int bar2;
};

int main()
{
    std::cout << sizeof(Base<Derived>) << '\n';
    std::cout << sizeof(Derived) << '\n';
    return 0;
}

both gcc and clang output 16 as both the sizeof(Derived) and sizeof(Base<Derived>), same result if i remove Base and implement it all in Derived as if they are all the same object godbolt example

the question is, why does this happen ? does the compiler simply combine both Base<Derived> and Derived into a single class when it sees CRTP being used ? or am i missing something ? how does the compiler get away from storing the extra pointers to the vtables in Derived and shrink its size to have only 1 pointer size overhead ? and is there any hidden overhead with this approach ?

i was expecting that the compiler would put a pointer to vtbl of Derived then the int then a pointer vtbl of Base then an int then a pointer to the vtbl of the interface in Derived bringing its size to 40 bytes, but this doesn't seem to be the case.


Solution

  • If you print out the offsets of the data members bar and bar2, it becomes obvious what's going on:

    std::cout << std::bit_cast<std::ptrdiff_t>(&Derived::bar) << '\n';
    std::cout << std::bit_cast<std::ptrdiff_t>(&Derived::bar2) << '\n';
    

    This prints:

    8
    12
    

    Judging by this, the layouts of the classes look like:

    0 1 2 3 4 5 6 7 8 9 A B C D E F
    [vptr         ] [bar  ] [     ] // Base<Derived>
    [vptr         ] [bar  ] [bar2 ] // Derived
    

    Note: vptr is the pointer to the vtable of the object.

    Remember that there is only one vtable pointer per polymorphic base class. When the derived class is initialized, it replaces the vptr in the base class subobject. See also: Where in memory is vtable stored?

    The last four bytes of Base<Derived> are padding, and Derived puts its bar2 member into that padding. This is allowed because Base<Derived> is a polymorphic class, and thus not standard-layout. As a result, even though Derived has one extra data member compared to Base<Derived> it does not take up any more space.

    If you added another int bar3 member, it should take up 24 bytes.