The Expressive Absence of Code
When we think about expressive code, we generally think about the code we write. But as I learned while watching Kate Gregory’s ACCU talk What Do We Mean When We Say Nothing At All?, expressive is also code that we don’t write.
What does that mean? What do we mean when we say What Do We Mean When We Say Nothing At All (note the meta-question)?
Examples of nothingness
C++ offers a growing set of keywords that allow you to express you intentions both to the compiler and to other humans that read your code.
Specifying your intentions to the compiler is useful for it to stop you when you don’t follow the intentions you declare. And specifying your intentions to humans is useful for your project to survive, essentially.
One of the simplest and oldest keyword in this regard is const
.
const
When you see a member function that is marked const
, it gives you a signal that the programmer who wrote it made a commitment that it wouldn’t modify the data member of its class.
Now what about a member function that is not marked const
? And especially if its name doesn’t suggest it should modify something, like if it’s called getX
or computeY
?
This can mean two things:
- the person who wrote it omitted the
const
on purpose, which means that you should be careful around this function because it’s modifying a state, contrary to what its name suggests, - or the person didn’t consider writing
const
, because it wasn’t in their habit. But the function doesn’t do anything fishy.
How do you know which one happened?
Kate Gregory suggests that, in the absence of other indications, you can infer this from the surrounding code. If there are const
s everywhere, it suggests that the omission at this specific place was made on purpose.
On the other hand, if there is no const
anywhere (and some C++ codebases have no const
at all, as she notes), this suggest that this doesn’t mean anything special for this function.
noexcept
Another keyword that raises the same type of question is noexcept
. If a function is not marked noexcept
, does this mean that it can throw exceptions, or that it doesn’t mean that the programmer didn’t think about marking it noexcept
?
Contrary to const
, noexcept
hasn’t always been there is language. It appeared in C++11. So any code that was written in C++98 or C++03 cannot have noexcept
s, just because it didn’t exist then.
explicit
If a class has a constructor that can be called with one parameter, and that constructor is not marked explicit
, then it allows implicit conversions from that parameters to the class.
In general, as Scott Meyers explains in item 5 of More Effective C++, you should stay away from implicit conversions.
If you come across a class that allows implicit conversion, the same type of question arises again: was this made on purpose? And again, you can guess with the same heuristics of looking around in the codebase.
This idea applies to various other keywords, such as public
, private
, override
, [[fallthrough]]
, [[maybe_unused]]
, and [[nodiscard]]
.
Avoiding defaults
After seeing the perspective of the reader of code, let’s know consider the choices we can make when writing the code.
With C++ keywords, a lot of defaults are arguably the wrong way around. For example, explicit
constructors should have been the default behaviour, with rather an implicit
keyword that lets you be explicit (pun not intended, but a good surprise) about your intentions.
Same thing for override
, [[fallthrough]]
, and so on.
For many keywords, they are keywords and not the default behaviour because of historical reasons: they were introduced during evaluations of the language. And to preserve backward compatibility they had to be optional keywords.
Seen this way, when you write code it’s better to avoid the defaults, when it’s reasonable.
For example, always use override
when you override a virtual function, always use noexcept
when your function cannot throw an exception, always use const
when you don’t plan for state change, and so on.
Consistency gives a signal
Using keywords to state your intentions is a natural thing to do, but Kate Gregory goes further and makes an interesting point about consistency when using such keywords.
Imagine that you’re taking over a project or a module that doesn’t use C++ keywords to express the programmer’s intentions. In this project, there is no trace of override
, [[fallthrough]]
, noexcept
, explicit
, [insert your favourite keyword here].
As we discussed earlier, this suggests that the person who wrote the code didn’t consider adding those keywords. So their absence at a given place doesn’t mean that they warned something surprising was going on.
Now as a person that leverages on C++ keywords to express your intentions, whenever you make a change your newly inherited module, you start using override
, explicit
and their little friends.
Over time, more and more keywords get sprinkled over the module.
Now a new person arrives and takes over the module after you. That person sees the oldest parts, with no explicit
and no const
and so on. But they also see the parts of the code you fixed, with the keywords. They inherit from a codebase that has inconsistent style in terms of keywords.
In that case, the style doesn’t tell anything any more: is a function that is not marked const
really const
because you (who know your stuff) purposefully omitted const
, or is it because an older developer didn’t think about it? Hard to tell without spending time git-blaming the code.
If you partially improve the code, you lose its consistency, and you lose the signal it gives.
A trade-off
Does this mean that you shouldn’t use the C++ keywords to express your intentions, in order to preserve consistency? Certainly not. This is probably what Jon Kalb would call a foolish consistency.
If you can update all the code to add the right C++ keywords at the right places, then great. It has the best value as you get both an expressive and consistent code.
But it also has a cost (even though some of it can probably be mitigated with automatic tools such as clang-tidy). And perhaps that value doesn’t justify the cost.
The point about consistency is that it gives a signal, and losing it also has a cost. You need to weigh that in when you compare the cost and the value of updating the whole code to add the right keywords.
A message to the future
There is a general idea behind all the individual considerations we discussed about writing or not writing a keyword. It’s about the signal you want to send to the future: show the person who will read your code in the future that you know what you’re doing.
Perhaps that will be by adding a keyword, or not adding it, or leaving a comment, or adding the reverse of a keyword, like noexcept(false)
for example.
But if you keep this guideline in mind, all the others will follow naturally, and you will get a better intuition to choose what to write and–perhaps as importantly–what not to write, in your code.
You will also like
- Simplicity in C++ Code (Podcast)
- How to Deal with Values That Are Both Input and Output
- Triple trip report from ACCU, C++ Russia and C++Now 2018
- A Concrete Example of Naming Consistency
Share this post!