You (probably) do want final
classes?
In the previous post I’ve discussed the C++11 final
keyword and how it can be used.
I also gave a guideline that you shouldn’t use final
on non-polymorphic classes.
My reasoning was as follows:
-
For some classes - like policy classes or any other class where you might want to have the EBO - making them
final
can be harmful. -
For other classes - those which aren’t used polymorphically -
final
is unnecessary. Every (good) C++ developer is taught early on that you shouldn’t use a class in a polymorphic inheritance hierarchy if it doesn’t have anyvirtual
functions.Public
inheritance there makes no sense and is harmful. Everybody knows that, thefinal
is just there to enforce it. -
There are only little use cases for
final
in polymorphic hierarchies. So in general you don’t need it.
This has spawned a discussion both on reddit and in the blog comments, so I decided to write this follow-up to unify the discussion and write about each argument.
For polymorphic classes: Final can be used to enforce MEC++ Item 33
Jon Kalb has reminded me in the previous post that final
can be used to enforce Item 33 of Modern Effective C++.
It states that “You should make non-leaf classes abstract”.
Scott argues that you shouldn’t inherit from non-abstract classes, a guideline I agree partly with.
If you decide to do so, you get a couple of problems and “bad” abstractions.
If you want to enforce that, you need a way to prevent inheritance from the leaf classes which aren’t abstract.
This is exactly what final
does.
Just mark every concrete class final
and you cannot violate that guideline.
For polymorphic classes: Final
can be used as an optimization
A couple of people mentioned that final
can help the compiler optimize, which is true.
There is a common compiler optimization called devirtualization.
Calling a virtual
function is more expensive then just calling a regular function:
the vptr
must be followed to get the right function pointer which must be dereferenced and call.
Those are indirections and indirections are expensive due to caches and other factors.
Furthermore: they cannot be inlined because of that. This is huge.
So if the compiler can get static information about which function needs to be called,
it can just insert a regular function call.
But when does the compiler know that?
After all, virtual
functions are there because you do not know which function to call.
It can happen when you have a function that doesn’t take the base class but a class that is lower in the hierarchy.
Is it lower or higher? I mean a class that is more derived than the base class.
In particular: If the class is a leaf. Consider the following translation unit:
struct base
{
virtual void foo() = 0;
...
};
struct derived : base
{
void foo() override;
...
};
void func(const derived &d)
{
d.foo();
}
Here the compiler has more information than if func
would just take base
.
In particular, if it can see that there is know type further derived than derived
,
it can devirtualize the call to foo()
because it knows that the type must be derived
or children
and that derived
has no children.
In bigger projects with multiple translation units this is more difficult to see than here.
For example, there could be another file that with a class more_derived
.
But sometimes link-time optimization can show that anyway.
Yet, putting final
on derived
let’s the compiler easily see that and thus encourage the optimization.
Okay, I’ve already said in the previous post that I just couldn’t come up with reasons for polymorphic classes. I knew about devirtualization but haven’t thought about it when writing. This is because I rarely use those classical OOP inheritance hierarchies.
Yes, standardese is centered around one. But it isn’t like my other projects.
So on to the more interesting discussion points: non-polymorphic classes and final
.
final
as wide vs narrow contract
/u/quicknir made an interesting comment on reddit that got more upvotes than my blog post itself, so many people seem to agree.
He wrote about wide vs narrow contracts.
If a class is final
, this is a narrow contract; you cannot inherit.
If you one day decide to remove the final
, it will widen the contract.
This isn’t a breaking change.
The other way round, however, is.
So when in doubt, always use final
on non-polymorphic classes; he argues.
He also wrote that my logic is based on the fact that non-final
is the default
and asked me to consider a situation where final
is default and there is a nonfinal
keyword.
Would I really argue for putting nonfinal
everywhere?
This got me thinking. I’m not someone who blindly keeps a position no matter the other arguments. If someone gives a good technical argument, I can sometimes switch sides.
And (unfortunately) this is a good technical argument.
So let’s consider a world where final
is default.
This world enforces the guideline that you shouldn’t inherit from non-polymorphic classes automatically.
As a class author you have to do active work in order to allow inheritance.
Arguably, this is a better world.
And in this world I would have learned C++ being taught that classes are final
by default.
And I would see the benefits of that approach.
Maybe in this world my main argument against final
- EBO - would be non-existent,
because it simply isn’t considered.
Of course I wouldn’t argue for nonfinal
everywhere.
Nobody would.
So yes, /u/quicknir is right, my argument comes from inertia.
So you should put final
on every non-polymorphic class?
So know I go and add final
onto every non-polymorphic class in my codebases, right?
I probably won’t.
I’m a library author, I don’t know how my classes are used.
Maybe someone has a good reason for (non-public
) inheritance.
While policy classes are a really tiny minority of classes, for me this isn’t true.
memory is centered around policy classes. Almost every class can be templated with an allocator. And those allocators can be empty, so inheritance is used to benefit from EBO.
And sadly the situation with EBO isn’t resolved.
While I agree that using EBO is a hack - like most C++ techniques to be honest,
it is still an essential part of my design.
As such, I prohibit final
on allocators and the other policy classes I use
and I still encourage you to do so for policy classes as well.
But I want a better solution that doesn’t conflict with final
.
There are some alternatives:
-
Simply allow
private
inheritance fromfinal
classes. This allows EBO usage but one can also argue that it breaks the use offinal
. -
Create a new attribute, like
[[maybe_empty]]
. If you put that on a class members, the compiler is allowed to give them zero size. But one could argue that this is “too big” for an attribute. -
Give
alignas(0)
a new meaning, currently it is ignored. Its meaning could be changed to give a class as member the size of0
. -
Create one of those “magic” standard library classes. A
std::compressed_pair
, similar to Boost’s but that can work onfinal
types. But I personally really hate standard library components a “normal” user cannot implement.
Maybe someone with experience in the standardization process can draft a proposal regarding EBO.
Given a proper solution to the situation of empty classes, my view on final
is going to change
and I will use it everywhere.
But as the current status is, I won’t go and add final
to my classes.
And if you do, please keep the EBO consequences in mind and don’t do it for policy classes.
It makes generic code easier.
This blog post was written for my old blog design and ported over. If there are any issues, please let me know.