Julien Jorge's Personal Website

Why `const` matters

Sun Jan 8, 2023

Arthur O’Dwyer published const all the things? a year ago. This is a very good article but I kind of disagree on some points. Let’s talk about them.

Why const is great

Using const on local variables is indeed not about performance and it may also clutter the code, but it sure as hell helps me to understand what is going on when I read some code, especially the code from others. See, when I try to understand an implementation I tend to execute the code in my head, and since I have only one CPU (brain) and a very limited L1 cache (short term memory), being able to dismiss or freeze some parts of the state leaves more room for the rest.

When I see int x, I prepare myself to see x changing before the end of the function. When I see const int x, I know I don’t have to keep track of its value. The same goes for const Widget* const w, I know that the code is about one and only one Widget, and that we are not going to modify its internals. This makes it easier to follow the actual computations.

In function signatures

When we talk about const in function signatures, we should distinguish two cases: A forward declaration (e.g. the signature in a header) and the function definition.

First of all let’s get rid of the obvious: large objects that are not expected to be modified must be passed by reference to const. We don’t want unneeded copies of large objects, so no passing by value, and we want to accept temporaries as arguments, so const& it is.

If the argument is a pointer to data that is not expected to be modified, then use a pointer to const. Using a non-const pointer would prevent the caller to pass a pointer to const.

Then we are left with arguments passed by value.

In a function declaration, declaring a value argument as const is absolutely useless. The caller does not care if the variable is going to be modified or not. Indeed, it is a local variable for the function, so any change is not visible by the caller.

But did you know that declaring value parameters as non-const in the declaration and const in the definition is valid C++? So one could write:

int foo(int);

// This is the definition of the function declared above.
int foo(const int i)
{
  return 2 * i;
}

Anyway, In a function definition, using const for value arguments does not help so much in terms of readability. As a general rule, a function should not modify its value arguments. This is something anyone should expect, so the const is superfluous. What sense does it make to use an argument in the body of a function if this argument is not actually the argument? (i.e. is has been modified in-between). Not so much sense, that is. Except maybe for large movable objects, which can be accepted by value because they are moved in the function’s body anyway.

Consequently, when I see a const value parameter, I can only wonder if the programmer has not forgotten a &.

Almost every point in the original article about const in function signatures is valid in my opinion, and I would disagree only on the following comment and code snippet:

std::string plus(const std::string& s, std::string& t);

The code above is also wrong, because it passes t by non-const reference. If t were really an out-parameter, it would be passed by pointer: std::string *t. The most likely explanation is that the programmer meant to pass by const reference and just forgot the const.

Systematically using a pointer for out-parameters is a bad practice as the caller will wonder if there must be a valid object or if nullptr can be passed. As soon as the argument is required, using a reference is the best solution. Indeed, it clearly tells the caller that there must be a valid object here. Unfortunately, the passing by address then becomes invisible on the call site. This may cause surprises to the programmer who just glanced the code there.

Data members

About const data members, the author is quite radical:

Const data members are never a good idea.

While I agree that, in general, const data members are problematic, as they prevent assignment and turn move-construction into copy-construction, I think there is at least one use case where it helps to mark data members as const. Consider the case of a class doing some incremental work with a given set of parameters, maybe something like that:

class incremental_scan
{
public:
  incremental_scan(std::string token, const std::string& document)
    : m_token(std::move(token)),
      m_document(document),
      m_search_index(0)
  {}

  std::size_t find_next()
  {
    const std::size_t r = m_document.find(m_token, m_search_index);

    if (r == std::string::npos)
      m_search_index = std::string::npos;
    else
      m_search_index = r + 1;

    return r;
  }

private:
  const std::string m_token;
  const std::string& m_document;
  std::size_t m_search_index;
};

We can discuss the relevance of being able to copy or move instances of incremental_scan, or remove the const and add some abstract stuff and inheritance to justify not being able to copy or to move. The point is that there exists some design where data members are either definitely set in the constructor or going to change during the life time of the instance. Putting const on the former makes the distinction clear. It is not about visibility or accessors, it is about telling the reader “you won’t see these member change in the thousands lines of code that implement this class, don’t worry about them.”

Conclusion: const is for the next reader

Using const is not about efficiency in the code, or optimizations. It is all about, as usual, communication with the next reader. As a programmer, you want the next reader to understand your intent easily, that’s why you will use const everytime you need to declare something that you are not going to modify; as long as it does not pessimize the program or prevent valid uses (see const data members above).

Mandatory exception: function arguments. You are not supposed to modify them, so maybe do not complexify the signature with unneeded consts.