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
const
s.