Jonathan Boccara's blog

How to Write Expressive Class Definitions

Published October 30, 2020 - 0 Comments

As developers, we read a lot of code. A typical code reading task is to scan through a class definition in a header file, in order to understand what the class is about.

Sometimes, the purpose of the class does not appear as clearly as we would like. Sometimes, we need to spend a bit of time scrutinizing the header file, in order to locate the meaningful bits of the class header that will help us figure out its responsibilities.

By putting ourselves in the shoes of a code reader (which shouldn’t be too hard since they are our own shoes really), we’ll see how to organize a class header so as to make life easier to the reader.

Surprisingly, that’s not always how we write code. Let’s start by analysing the natural way to structure a class header.

I’m not sure what’s the natural way for everyone, so I’ll go through what feels natural to me, assuming that it must be natural to at least some other people too, especially since I’ve seen a lot of code structured this way.

(heads-up: I will argue afterwards that any time you see the word natural in the following section, you can replace it by the word wrong).

The natural way to define a class

Let’s take the example of a class that represents a circle.

The first thing we write is that it’s a class and give it a name:

class Circle
{

Note that we choose to use class over struct because it represents an object that does things rather than a bundle of information.

Then I’ll define the public section:

public:

What to add first in the public section of the class? What the first thing you need to do with an object of this class? Or with an object of any class, for that matter?

Construct it!

So let’s put the constructor first. That’s so natural.

A circle can be built from a radius, and say that we don’t want to allow circles to be built with no radius.

If we just define the constructor with the radius parameter, the compiler won’t add a default constructor, so we don’t need to write Circle() = delete.

But if we don’t write that line, by extending Kate Gregory’s argument on the expressive absence of code a reader of our interface could wonder: did the author omit the default constructor because they didn’t want the circle to be constructible by default, or did they just forgot it?

So let’s go all the way and add the line Circle() = delete; in order to clarify our intentions.

Now in which order should we define our constructors? The natural order here is to start by the default constructor, because… it’s the “default” one, right?

    Circle() = delete;
    explicit Circle(double radius);

We don’t need to write copy, move, and destructors because the compiler will handle it. But let’s say that we want our circle to be swappable.

Swapping, being related to life cycle management, is in the same family of operations as copying and moving. The natural position to put it is here, towards the beginning of the class definition, just after the constructors:

    friend void swap(Circle& lhs, Circle& rhs) noexcept;

Okay, now that all the life cycle operations are out of the way, let’s add the specific responsibilites of the Circle class:

    double perimeter() const noexcept;
    double area() const noexcept;
    void growAreaBy(double factor) noexcept;

And let’s finish with the private stuff:

private:
    double radius_;
};

In summary, our natural class definition looks like this:

class Circle
{
public:
    Circle() = delete;
    explicit Circle(double radius);
    friend void swap(Circle& lhs, Circle& rhs) noexcept;

    double perimeter() const noexcept;
    double area() const noexcept;
    void growAreaBy(double factor) noexcept;

private:
    double radius_;
};

A clearer way to lay out a class definition

As hinted above, you can replace every occurrence of the word natural in the above section by the word wrong.

The natural decisions above were the following:

  • put the constructors first,
  • put the deleted default constructor before the other constructor,
  • put swap towards the beginning of the class definition,
  • put the class responsibilities at the end of the public section.

Why are those decisions wrong? Because they make sense for code writers, and not code readers.

But since we read code much more often than we write it, there are many more occasions where we are a code reader than a code writer. So those decisions are sub-optimal.

Expressive code is made in the rare times we write code, for the many times we read it.

When you read code, the class constructors generally don’t matter. Indeed, if you’re reading code that compiles, and that uses an object of class X, then you’ll know that an object of type X has been correctly constructed.

What’s more interesting is what X is about. And this is what the class responsibilities tell.

As we realized when seeing the difference between struct and class, what defines a class is its interface. A class can do things. What defines our Circle class is that it can compute its perimeter(), its area() and that it can resize to growAreaBy a certain factor.

As code readers, this is much more meaningful than whether Circle can be constructed by default or not. This is useful info for code writers only, so it has less priority. For that reason, we want to put constructors after the class responsibilities.

swap is even less relevant, because code writers need the constructors more often than they need swap. So swap should go at the very end of the class definition.

Amongst the constructors, the way we ordered them initially was to put the default deleted one first, but this was also not expressive.

Indeed, the message we wanted to get across when writing the interface was: “A circle can be built from a radius. By the way, it doesn’t make sense to build a circle our of nothingness”.

This translates into the following code:

    explicit Circle(double radius);
    Circle() = delete;

What we wrote instead was this:

    Circle() = delete;
    explicit Circle(double radius);

Which means: “Let’s start by telling you how NOT to build a circle.” This is confusing.

In summary, a better way to order the class definition is this:

class Circle
{
public:
    double perimeter() const noexcept;
    double area() const noexcept;
    void growAreaBy(double factor) noexcept;

    explicit Circle(double radius);
    Circle() = delete;
    friend void swap(Circle& lhs, Circle& rhs) noexcept;

private:
    double radius_;
};

This way, a reader gets the meaningful information about the class responsibilities right from the start, and the life cycle management is left at the end of the public section.

The difference gets bigger with larger classes than our candid Circle class.

Other poor layout practices

Following the idea of putting the meaningful information first, there are two other practices that exist in code but that make it less expressive: private section first and method bodies in the definition.

private section first

In C++, class members are private by default. This means that the following class is equivalent to our previous Circle class:

class Circle
{
    double radius_;

public:
    double perimeter() const noexcept;
    double area() const noexcept;
    void growAreaBy(double factor) noexcept;

    explicit Circle(double radius);
    Circle() = delete;
    friend void swap(Circle& lhs, Circle& rhs) noexcept;
};

I suppose the point of this practice is to save one line of code and a handful of characters, because we no longer have to write the private: mention .

But this hinders readability, because the code reader is greeted with the private section of the class, which are implementation details. We should avoid that.

Method bodies in the definition

Another way of coding the Circle class is to implement the body of the class member functions directly in the class definition:

class Circle
{
public:
    double perimeter() const noexcept
    {
        return 2 * Pi * radius_;
    }
    double area() const noexcept
    {
        return Pi * radius_ * radius_;
    }
    void growAreaBy(double factor) noexcept
    {
        radius_ *= sqrt(factor);
    }

    Circle() = delete;
    
    explicit Circle(double radius) : radius_(radius) {}
    
    friend void swap(Circle& lhs, Circle& rhs) noexcept
    {
        std::swap(lhs.radius_, rhs.radius_);
    }

private:
    double radius_;
};

If your class is in a header file, there is a high chance that this is a bad idea. Indeed, this presentation overwhelms the reader with implementation details, clouding the big picture for what the class is about.

It can make sense to mix class definition and methods declaration in very local classes though, for example in functors used in the STL (indeed, even with the addition of lambdas to the language, functors are not dead).

But in the general case, we should go the extra mile and have those definition in a separate file:

// in Circle.cpp

double Circle::perimeter() const noexcept
{
    return 2 * Pi * radius_;
}
double Circle::area() const noexcept
{
    return Pi * radius_ * radius_;
}
void Circle::growAreaBy(double factor) noexcept
{
    radius_ *= sqrt(factor);
}

Circle::Circle(double radius) : radius_(radius) {}

void swap(Circle& lhs, Circle& rhs) noexcept
{
    std::swap(lhs.radius_, rhs.radius_);
}

If you’re refraining from extracting the code in a separate file because you’d like the member function bodies to be inline, you can still have them follow the class definition in the header file, or even better put them in another header file included after the class definition:

// Circle.hpp

class Circle
{
public:
    double perimeter() const noexcept;
    double area() const noexcept;
    void growAreaBy(double factor) noexcept;

    explicit Circle(double radius);
    Circle() = delete;
    friend void swap(Circle& lhs, Circle& rhs) noexcept;

private:
    double radius_;
};

#include "Circle.inl.hpp"

And Circle.inl.hpp would contain:

// Circle.inl.hpp

inline double Circle::perimeter() const noexcept
{
    return 2 * Pi * radius_;
}

inline double Circle::area() const noexcept
{
    return Pi * radius_ * radius_;
}

inline void Circle::growAreaBy(double factor) noexcept
{
    radius_ *= sqrt(factor);
}

inline Circle::Circle(double radius) : radius_(radius) {}

inline void swap(Circle& lhs, Circle& rhs) noexcept
{
    std::swap(lhs.radius_, rhs.radius_);
}

Note the addition of the inline keyword.

Worry about your readers

Writing expressive code is about getting the right message to the readers of your code.

By organizing your class definition in a way that will make the meaningful information stand out, you’ll make your code less difficult to read, and your application less difficult to maintain.

You will also like

Don't want to miss out ? Follow:   twitterlinkedinrss
Share this post!Facebooktwitterlinkedin