Multiple Inheritance and the Dreaded Diamond / by Joel Goodman

I make no secret of the fact that I love OOP. It allows us to make our code reusable and in doing so shrinks and simplifies our codebases, and it pairs well with other paradigms like FP to help us make our code more expressive. And it's impossible to deny the fact that there is a sort of objective (pun intended) beauty to the idea that we can represent the real world and the things in it with OOP.

OOP is powerful, and powerful things can get you in trouble. I was recently having a discussion with another engineer and the diamond problem came up, which is an issue that arises from an abuse of OOP. This abuse is known as multiple inheritance, and it is the root of all evil.

The Problem

Imagine we have a class A:

class A
{
public:
  auto foo() const -> void
  {
    std::cout << "A " << __func__ << std::endl;
  }

  A()
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }
};

Now let's say we have two more classes, B and C, which are both As.

class B : public A
{};

class C : public A
{};

Now let's imagine we have yet another class called D, which is both a B and a C.

class D : public B, public C
{};

Now let's write a main() that creates a D and calls foo().

auto main() -> int
{
  D d;
  d.foo();
}

When we attempt to compile this, we'll get an error that looks a little like this:

error: non-static member 'foo' found in multiple base-class
      subobjects of type 'A':
    class D -> class B -> class A
    class D -> class C -> class A
    d.foo();
      ^
note: member found by ambiguous name lookup
  auto foo() const -> void
       ^
1 error generated.

...and we have an abiguity. The compiler doesn't know if we're asking for a B::foo() or a C::foo(). This is a problem. How can we avoid this?

The Solution

Well, one way is to specify which foo() you want. It feels clunky, but if we rewrite main() to look like this

auto main() -> int
{
  D d;
  d.B::foo();
  b.C::foo();
}

we get this output:

Constructing A
Constructing A
A foo
A foo

If we're really explicit about which foo() we want, we can pull it off. But this just feels... wrong. Plus, the output of the program highlights another problem, that each D gets two As, one for its B and one for its C, and the contents of each A is identical. This is the root of the problem. If we had only one A, the abiguity would be resolved.

The answer is to make B and C public virtual As. By using virtual inheritance, we allow the compiler to create a vtable with just a single field for A::foo(), and guarantee the construction of just one A for every D we instantiate. We don't even need to make A::foo() a virtual function. So B and C look like this now:

class B : public virtual A
{};

class C : public virtual A
{};

This minor change makes all the difference in the world. We can now just call d.foo() directly.

auto main() -> int
{
  D d;
  d.foo();
}

The code above yeilds the following:

Constructing A
A foo

So now we have one A and thus just one foo(). Nice! But what if A has some data that we need to initialize it with?

class A
{
public:
  const std::string name;

  virtual auto foo() const -> void
  {
    std::cout << "A " << __func__ << std::endl;
  }

  A(const std::string new_name="NONE") : name{new_name}
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }

};

In this case all we need to do is initialize it with D, since A() has a default value set for new_name.

class D : public B, public C
{
public:

  D(const std::string new_name) : A(new_name)
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }

};

So this:

auto main() -> int
{
  D d("Jojo");

  std::cout << d.name << std::endl;
  std::cout << d.B::name << std::endl;
  std::cout << d.C::name << std::endl;

}

Yeilds this:

Constructing A
Constructing D
Jojo
Jojo
Jojo

As you can see, the B and C that belong to d got the same name. If there isn't a default constructor for A, things get more complicated. But only slightly, as you'll see below. The classes below are subtly changed to compensate for the fact that A can't be default initialized.

class A
{
public:
  const std::string name;

  virtual auto foo() const -> void
  {
    std::cout << "A " << __func__ << std::endl;
  }

  A(const std::string new_name) : name{new_name}
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }

};

class B : public virtual A
{
public:
  B(const std::string new_name="") : A(new_name)
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }
};

class C : public virtual A
{
public:
  C(const std::string new_name="") : A(new_name)
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }
};

class D : public B, public C
{
public:

  D(const std::string new_name) : A(new_name), B(), C()
  {
    std::cout << "Constructing " << __func__ << std::endl;
  }
};

And the main() below is the same as last time.

auto main() -> int
{
  D d("Jojo");

  std::cout << d.name << std::endl;
  std::cout << d.B::name << std::endl;
  std::cout << d.C::name << std::endl;

}

And the output is similar.

Constructing A
Constructing B
Constructing C
Constructing D
Jojo
Jojo
Jojo

Really, when it comes down to it it's best to just give A some kind of default initialization.

Conclusion

Virtual inheritance is very useful for preventing an awful problem that could also be prevented by favoring composition over inheritance, which I might write up a little blurb on at some point in the near future. And the virtual keyword and its various uses are super intersting, which also make it tempting to write about.

That's all! Hope someone finds this useful!