Default parameters and virtual (C++20)
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 useoverride
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 callBase::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.