Jonathan Boccara's blog

The Expressive Absence of Code

Published May 21, 2019 - 0 Comments

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 consts 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 noexcepts, 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

Become a Patron!
Share this post! Facebooktwittergoogle_pluslinkedin    Don't want to miss out ? Follow:   twitterlinkedinrss

Get a free ebook on C++ smart pointers

Get a free ebook of more than 50 pages that will teach you the basic, medium and advanced aspects of C++ smart pointers, by subscribing to our mailing list! On top of that, you will also receive regular updates to make your code more expressive.

No spam. You can unsubscribe at any time.