Dominant Virtual Functions in C++ Explained

This article explores the concept of dominant virtual functions and the dominance rule within virtual inheritance hierarchies in C++. It covers why the dominance rule exists, how the C++ compiler uses it to resolve potential name lookup ambiguities in diamond inheritance patterns, and the exact conditions under which a member function dominates another. You will also see a concrete code example demonstrating how dominance works in practice and learn what happens when an override conflict occurs that dominance cannot resolve.

The Problem: Diamond Inheritance and Ambiguity

In standard multiple inheritance, combining two classes that share a common ancestor leads to the classic "diamond problem." If both intermediate classes inherit a member from the base class, the most derived class receives two distinct copies of that base class, resulting in ambiguity whenever that member is accessed.

Using virtual base classes solves the problem of multiple subobjects by ensuring that only one shared instance of the base class exists in the hierarchy. However, virtual inheritance introduces a new question for virtual function dispatch: if one branch of the inheritance hierarchy overrides a virtual function from the virtual base, but another branch does not, which function should the most derived class call?

Without a specific rule, the compiler might see two viable paths to the function—the original definition in the base class and the override in the intermediate class—leading to an ambiguity error. C++ resolves this using the dominance rule.

The C++ Dominance Rule

The C++ standard defines dominance to handle member lookup in directed acyclic graphs formed by virtual inheritance.

Under the dominance rule, a declaration of a member (such as a virtual function) in a class B dominates a declaration in a class A if A is a base class of B.

When resolving a function call in a derived class:

  1. The compiler identifies all reachable declarations of the virtual function across the inheritance hierarchy.
  2. If one declaration resides in a class derived from the class containing another declaration, the derived class's declaration dominates the base class's declaration.
  3. Dominant declarations eliminate dominated declarations from the candidate set during lookup.
  4. If exactly one dominant declaration remains, that function is chosen without any ambiguity error or compiler warning.

Code Example of Function Dominance

Consider the following hierarchy:

#include <iostream>

struct Base {
    virtual void show() {
        std::cout << "Base::show\n";
    }
    virtual ~Base() = default;
};

// Left branch overrides show()
struct Left : virtual Base {
    void show() override {
        std::cout << "Left::show\n";
    }
};

// Right branch does not override show()
struct Right : virtual Base {
};

// Most derived class inherits from both
struct Derived : Left, Right {
};

int main() {
    Derived d;
    d.show(); // Calls Left::show()
}

In this example:

  • Base defines show().
  • Left virtually inherits from Base and overrides show().
  • Right virtually inherits from Base but does not override show().
  • Derived inherits from both Left and Right.

When d.show() is called, the compiler finds two declarations: Base::show() (via Right) and Left::show() (via Left). Because Base is a base class of Left, Left::show() dominates Base::show(). The dominated function Base::show() is discarded, and Left::show() is called cleanly without ambiguity.

When Dominance Does Not Apply: Ambiguous Overrides

The dominance rule only resolves conflicts when one candidate belongs to a class derived from the other candidate's class. It does not resolve conflicts between two declarations on sibling branches of the hierarchy.

If both Left and Right override show():

struct Left : virtual Base {
    void show() override { std::cout << "Left::show\n"; }
};

struct Right : virtual Base {
    void show() override { std::cout << "Right::show\n"; }
};

struct Derived : Left, Right {
    // Compile error: ambiguous call if show() is not overridden here
};

In this scenario:

  • Left::show() dominates Base::show().
  • Right::show() also dominates Base::show().
  • Neither Left nor Right is derived from the other, so neither dominates between the two.

Because two dominant functions remain, the compiler rejects the program due to ambiguity. To fix this, Derived must provide its own final override of show(), which in turn dominates both Left::show() and Right::show().