CS247 Lecture 19

Last Time: LI in SOLID, Multiple inheritance

This Time: More multiple inheritance, ID in SOLID

As such, we cannot use classes with private or protected inheritance polymorphically.

A* p = new B{...}; // only legal under public inheritance

Multiple inheritance CAN have some tricky semantics.

class A1{
	public:
		void foo(){cout << "A1::foo" << endl;}
};
 
class A2{
	public:
		void foo(){cout << "A2::foo" << endl;}
};
 
class B: public A1, public A2 {}
B b{};
b.foo(); // What should happen?

Ambiguous - it’s not clear whether A1::foo or A2::foo should be called - b.food() doesn’t compile. Disambiguate: b.A1::foo() or b.A2::fooo();

Question

Can you do this if b inherits from a single class, say class B: public A, can you do B.A::foo?

Although not needed, yes you can do this! It’s using the Scope Resolution Operator.

Another tricky aspect of multiple inheritance - shared Base class. “Diamond Problem”, “deadly diamond of death”.

struct A{int a = 1;}
struct B: A{}; 
struct C: A{}; // Don't need to specify public - structs have public inheritance by default
struct D: B, C{};
D dObj;
dObj.a = 2; // Doesn't compile either!

Because a D is-a B and a C, it ends up having 2 a fields - one from the B portion of the object and one from the C portion of the object.

Must instead disambiguate - which a field are we talking about? The one from the B portion, or the one from the C portion?

dObj.B::a = 1; // Setting different a fields in the `D` object
dObj.C::a = 2;

Wha if we wanted to disable this (somewhat strange) behaviour, and have only copy of A in our hierarchy? Solution: Use Virtual Inheritance.

struct B: A -> struct B: virtual A{...}
class C: public A -> class C: virtual public A{...}

Syntax: struct B: virtual A{...} class C: virtual public A{...}

"virtual" inheritance

The keyword “virtual” indicates that this base class will be shared among all others who inherit from it virtually in the inheritance hierarchy.

Now: dObj.a → unambiguous, a field is now shared between both portions of the object.

class A {
	public:
		A(int x) {...}
};
 
class B: public virtual A {
	public:
		B(...):A{1}.. {}
};
 
class C: public virtual A{
	public:
		C(...): A{2} {...}
};
 
class D: public B, public C {...}

Constructors for virtual bases must run before any other constructors. (Just remember this.)

Note

In this example, class A is virtually inherited by both classes B and C. When a class virtually inherits from a base class, the virtual base class is constructed before any non-virtual derived class constructors. This ensures that there is only one instance of the virtual base class shared among all the classes that virtually inherit from it.

Why do constructors for virtual bases run before any other constructors? This is because the compiler needs to guarantee that the shared base class is initialized properly and that its members are ready to be used by the constructors of all the classes that virtually inherit from it.

Typically:

Virtual inheritance:

  • This shows the order in which the objects is constructed for virtual inheritance.

Virtual inheritance (idk what’s going on…)

  1. Call’s D constructor first
  2. D calls A’s constructor
  3. Return to D
  4. Call’s B constructor
  5. Return to D
  6. Call C
  7. Return to D look at picture above.

Fixed:

class A{
	public:
		A(int x): ... {...}
};
 
class B: public virtual A{
	public:
		B(): ... {...} // Calls A's constructor
	}; // Similar to C
 
class C: public virtual A{
	public:
		C(): ... {...} // Calls A's constructor
}
 
class D: public B, public C{
	public:
		D()A{5},B{}, C{}... {...} // B{}, C{} could be left out
};

In this corrected version:

  • Class B and C are derived virtually from A, ensuring that only a single instance of A is present in the inheritance hierarchy.
  • In class B and C, the constructors call the constructor of A using their member initialization lists.
  • In class D, the constructor initializes the virtual base class A explicitly with A{5}, and the default constructors of B and C are called using B{} and C{} respectively.

This code follows the correct order of constructor execution, ensuring that the virtual base class A is constructed before the constructors of derived classes (B and C) are executed. This maintains the integrity and correctness of the inheritance hierarchy.

Destruction sequence/steps

The destruction steps for the classes A, B, C, and D would follow the reverse order of construction, with the destructor of each class being called in the reverse order of their constructor execution. Here’s the order in which the destructors would be called:

  1. Destructor of class D.
  2. Destructor of class B (via the virtual inheritance from A).
  3. Destructor of class C (via the virtual inheritance from A).
  4. Destructor of class A.

What about object layout with multiple virtual inheritance???

class A{...}
class B: public A{...}

B object:

B* bp = new B{...};
A* ap = bp;

To treat a B* like an A*, simply create a pointer copy, prints at the same location, ignore B fields. Same strategy cannot be used for multiple virtual inheritance.

D* dp = new D{...}
A* ap = dp;
B* bp = dp;
C* cp = dp; // The issue is here, does not look like an C object

C++/clang:

Note

virtual Base(A) is actually stored at the end of the object! Not the beginning.

B* bp = ...;
cout << bp->a << endl;

If bp points at a D object, see above memory diagram. If bp points at a B object,

If w’re pointing at a D object, then bp->a is 40 bytes below the pointer address. If we’re pointing at a B object, then bp->a is 24 bytes below the pointer address.

Note

Now, finding superclass fields depends on the dynamic type.

Solution: Store the offset to the superclass fields inside the vtable.

This is where the name “virtual” inheritance comes from.?

Note

The D object does not look like an A object, a C object or a B object simultaneously, but portions of it do.

Doing pointer assignment can change where the pointer is pointing to.

  • This is really IMPORTANT, be very careful
D* dp = new D{...};
A* ap = dp; // Points at the top of the `D` object
// ap - points just at the A portion

static_cast / dynamic_cast will also adjust pointer locations for you as necessary.

reinterpret_cast won’t - underscores danger associated with the cast.

Finally, both Google C++ guide, and ISOCPP both recommend when using multiple inheritance, interface classes are best.

Interface classes are those that:

  • Define all methods as pure virtual
  • Contain no state (fields)

Next: CS247 Lecture 20