loading...
Cover image for The Many Masks of `const`

The Many Masks of `const`

somedood profile image Basti Ortiz (Some Dood) ・3 min read

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!

Posted on by:

somedood profile

Basti Ortiz (Some Dood)

@somedood

Just some dood trying to make code work without bringing the Universe to its demise.

Discussion

markdown guide
 

Impossible, eh? Strong words.

On a member function, the suffix const to a method declaration makes the implicit this argument itself const:

class Foo {
private:
  int m_field;
public:
  int field() const { return m_field; }
};

That Foo::field() is really:

int Foo_field(Foo * const this) {
  return this->m_field;
}

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:

class Foo {
private:
  int m_field;
public:
  int field() const { return ++const_cast<Foo *>(this)->m_field; }
};

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:

class Foo {
private:
  mutable int m_field;
public:
  int field() const { return ++m_field; }
};

The mutable keyword prevents the effect of a const this 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:

class Foo {
private:
    int m_field = 0;
    std::mutex m_mutex;
public:
    int field() const {
        std::lock_guard<std::mutex> l__inst(m_mutex);
        return m_field;
    }
};

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" the const 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 ;)

 

Reading variable declarations backwards will greatly help in remembering these nuances

This is the best advice which I've ever seen to remember these ambiguities in definition of const pointers

 

Thanks, man! All the credit goes to the guy who wrote that amazing StackOverflow answer.