I was messing around with some code that implements a widget hierarchy today, and bumped into some interesting edge cases of virtual functions. Parameters with a default value behave in an unexpected way.

Here is a vastly simplified code snippet:

#include <fmt/core.h>
struct Base { virtual void Func(int v = 42) = 0; };
struct DerivedA : public Base {
  void Func(int v) override { fmt::print("A={}\n", v); }
};
struct DerivedB : public Base {
  void Func(int v = 8) override { fmt::print("B={}\n", v); }
};

There is a abstract base class (with a virtual function defined = 0) and two derived classes which override the virtual function in the base. Note that one of the derived classes forgets the default value of the parameter, and the other gives it a different value.

Note the use of the contextual keyword override. See item 12 in Scott Meyers Effective Modern C++. It makes the compiler complain in the function declaration that is decorated with it, is not actually an override. Examples of not-an-override come from typo’s, different return or parameter types, different constness .. there’s a bunch of ways to get it wrong, which is why the advice is to use override liberally.

Let’s call those virtual functions via a pointer-to-base and a pointer-to-derived in all four possible variations, shall we?

int main() {
  Base * ba = new DerivedA;
  Base * bb = new DerivedB;
  auto * da = new DerivedA;
  auto * db = new DerivedB;
  ba->Func();
  bb->Func();
  da->Func(3);
  db->Func();
}

You may ask: why does da->Func() need a parameter? Well, there is no default given in the declaration in the derived class. The default value provided in the base class is hidden.

If I leave the value 3 out, then clang suggests that I call Base::Func() instead. That compiles, and then fails to link because – and that’s the whole point – Base::Func() is pure virtual.

The output of the program is this:

A=42
B=42
A=3
B=8

When called through a pointer-to-base, the default value from the declaration in the base class is used. When called through a pointer-to-derived, the default value from the declaration in that derived class is used (or, if there is none, then you need to provide a value).

Takeaway

Now that I ran into this, I looked it up on cppreference, which says

The overriders of virtual functions do not acquire the default arguments from the base class declarations, and when the virtual function call is made, the default arguments are decided based on the static type of the object.

In the context of the codebase I’m working on today, this translates to

Do not provide default arguments in the declaration of abstract virtual functions.