Immutability is often considered to be a "best practice" in many programming languages. It reduces the likelihood of introducing unwanted side effects to the code base, thus making it less prone to bugs.
As a testament to this "best practice", variables in Rust are immutable by default. One has to go through the hassle of explicitly using the mut
keyword to declare a mutable variable.
C++, a language (famously and infamously) known for its reliance on state and mutability, is no exception to this "best practice". Like many other languages, the const
keyword is used to declare a constant "variable". However, the const
keyword is not as simple as it is in other languages. In C++, it has a plethora of meanings, each depending on how it is used per situation. In a way, it can be said that—similar to having multiple split personalities—the const
keyword wears many masks.
The Straightforward Way
The simplest, most familiar, and most intuitive way of using the const
keyword is to declare constants. Like in many other programming languages, any variable (or function parameter) declared as const
cannot be reassigned.
int main() {
// Mutable variable
int variable = 5;
// Immutable constant
const int constant = 10;
// This is legal
variable = 1;
// Throws an error
constant = 25;
return 0;
}
Class Fields
Similar to variables, class fields can also be declared constant. Any object instantiated from this class will have its respective properties immutable.
#include <string>
class Dog {
public:
std::string name;
const std::string breed;
Dog(const std::string& dogName, const std::string& dogBreed)
: name(dogName)
, breed(dogBreed)
{ }
};
int main() {
Dog presto("Presto", "Siberian Husky");
// This is legal
presto.name = "Not Presto";
// Throws an error
presto.breed = "Not a Husky";
return 0;
}
Class Methods
Class methods can also be declared const
. By doing so, a class method promises to never mutate any of its class' fields.
#include <string>
class Dog {
private:
std::string name;
public:
Dog(const std::string& dogName) : name(dogName) { }
// This is a `const` method because it does not
// mutate any of the fields.
std::string getName() const { return name; }
// On the other hand, this setter method
// can **not** be `const` because it
// mutates/sets a field to a value.
void setName(const std::string& newName) { name = newName; }
};
Pointers
The const
keyword becomes quite confusing with pointers. At first glance, it seems to be a simple idea. However, using the const
keyword on pointers begs the question of what exactly is being declared const
. Is it the actual pointer to the memory address or is it the dereferenced value of the memory address? For this dilemma, C++ has a way to specify which is to be declared const
.
int main() {
int foo = 1;
int bar = 2;
int baz = 3;
// Constant actual pointer
int* const actualPointer = &foo;
// Constant dereferenced value
const int* dereferValue = &bar;
// Constant pointer **and** dereferenced value
const int* const both = &baz;
return 0;
}
Reading variable declarations backwards will greatly help in remembering these nuances. This StackOverflow answer summarizes it very well and provides more advanced examples and exercises. For beginners and veterans alike, this answer is a must-read for anyone working with C++.
Conclusion
The const
keyword is more than just a mechanism for enforcing immutability. It is more than just a safety net from unwanted side effects. For developers, it is an annotation that hints intentions. It is an implicit documentation of how a function (or variable) works. For example, a const
getter method assures a developer that it will not mess up their code in any way because it is impossible to do so under const
constraints.
One can argue that adding multiple const
declarations all over the code base is unnecessarily verbose. Yes, that is a valid argument, but the benefits provided by implicit documentation and the safety of immutability simply outweigh the drawbacks of verbosity (at least for me). Besides, it is considered to be a "best practice" for a good reason.
So go and decorate your code base with as many const
declarations as necessary!
Top comments (14)
Impossible, eh? Strong words.
On a member function, the suffix const to a method declaration makes the implicit
this
argument itself const:That Foo::field() is really:
Since
this
is const, the field is also constant.So far, that's what you said. But C++ is well known for giving the programmer ample opportunity to shoot themselves in the foot.
One option is the const-cast:
Which is demonstrably evil. The developer has blown away the const across the whole object. Like all casts, this isn't a safe thing to do, and needs careful validation - but there are niche cases where it might be the only solution to a problem.
A more controlled mechanism is to use
mutable
:The
mutable
keyword prevents the effect of a constthis
pointer, but only for a particular field. Now the field can be changed legally within any const method. Which is, on the face of it, a Bad Thing.But there are a few legitimate cases for doing this. A good example is where you want to hold a lock during a read of a constant object. std::mutex::lock is a non-const method, so the
std::lock_guard
uses a non-const reference. All of which means this won't work:The solution is to make that mutex mutable - then the lock guard works and you're thread-safe.
But more importantly, while the method isn't "memory-const" anymore, it remains "semantically-const". To put it another way, unlike the previous examples I gave, this code behaves like you'd expect.
Oh, boiii. That looks pretty messy if you'd ask me. I just love how C++ can give you an infinite number of ways to shoot yourself in the foot. This is really funny, yet concerning. 😬
I wouldn't really count the
mutable
keyword since that's an actual feature of the language that allows you to "bypass" theconst
declaration.However, I would definitely count the crazy type-casting. That, right there, is why you have to love and hate C++ at the same time.
Thanks for sharing this! This is genuinely one of the more interesting comments I've seen in this site for a while.
Glad you found it interesting.
But the casts aren't inherently evil - they're a factor of C++ giving you all the tools you might need.
I was just looking for a reference about this yesterday. Bravo!
Dang, what are the odds of you coincidentally stumbling upon this article? That's amazing. 😁
Not much of one. I was poking around Dev this morning, thought "Huh, I wonder what new stuff Some Dood has written," and there it is!
It sounds so silly to say "I wonder what new stuff some dood has written" because of how vague and ambiguous it sounds when out of context. 😆 If someone outside the community read that, they would definitely ask what you meant by "some dood"? Ah, this is why I love this username. It just sounds so silly.
Irrelevant, but one social IRC room I hang out in is inhabited entirely by Python programmers. We maintain an entire gallery of statements that come up during conversation, but sound ridiculous out-of-context.
Yo, that sounds so funny. I'd love to read the humor of statements without context.
Anyway, we seem to be drifting quite far from your original comment. 😂 I wouldn't want to go too off-topic here. Thanks again for your nice comments! They really mean a lot.
Follow me back, and we can take the conversation over to PM, yeah?
GOOD IDEA! Why have I never thought about that before? I'll see you on the other side, my friend.
Thanks for the reminders!
Also of note: in many embedded systems const items stay in ROM, which provides runtime protection from bad people too ;)
This is the best advice which I've ever seen to remember these ambiguities in definition of
const
pointersThanks, man! All the credit goes to the guy who wrote that amazing StackOverflow answer.