Search code examples
c++inheritancecompiler-constructionruntime

How are base pointers able to know the memory location of base members in a derived class instance?


Suppose the following code with basic structures

struct A {int aMember};
struct B {bool bMember};
struct C {double cMember};
struct BA : B, A {};
struct CB : C, B {} ;

void test(B *base) {
    bool retrieved = base->bMember;
}

Here the test function is able to be passed a pointer to an instance of B, BA, or CB. How is the retrieval of the base class member "bMember" achieved in low-level terms? Presumably the member can't be guaranteed to be located at a given offset from the passed objects addressee for every derived type. In short, how is it "known" where the slice of B's members are located for any given derived type? Is this achieved at run-time with some sort of meta-data associated with objects and classes utilizing inheritance?

I'm terribly sorry if there's a simple explanation already posted. I just didn't know how to phrase my search to return a relevant answer.

Thankyou!


Solution

  • A given member of B is always at the same offset from base, because base is a B*.

    The piece of the puzzle I think you're missing is that test isn't passed the address of the object itself when you're passing a BA* or a CB*.
    Instead, it's passed the address of their respective subobjects of type B.

    Example using your classes:

    void test(B *base) {
        std::cout << "test got " << base << std::endl;
    }
    
    int main()
    {
        BA ba;
        CB cb;
        std::cout << "&ba: " << &ba << std::endl;
        std::cout << "ba's B subobject: " << static_cast<B*>(&ba) << std::endl;
        test(&ba);
        std::cout << "&cb: " << &cb << std::endl;
        std::cout << "cb's B subobject: " << static_cast<B*>(&cb) << std::endl;
        test(&cb);
    }
    

    For me, this printed

    &ba: 0x28cc78
    ba's B subobject: 0x28cc78
    test got 0x28cc78
    &cb: 0x28cc68
    cb's B subobject: 0x28cc70
    test got 0x28cc70
    

    Both calls to test pass the B subobject to the function, and every B object looks the same, so test doesn't need to care about the other classes at all.

    Note that ba and ba's B subobject are in the same place; this particular implementation arranges subobject in the order they're specified in the inheritance list, and the first one is located first in the derived object.
    (This isn't mandated by the standard, but it's a very common layout.)