One of the most amazing features of C++ is something called const-correctness. In C++, the keyword
const may be used as part of a variable's type information. In essence, it means that from that variable, you may only observe the object's state, but not change the object's state. It is an implicit conversion from non-const to const, but to go backwards - from const to non-const - requires an explicit cast via the use of
const_cast and may not even be safe! In certain cases the object referred to may actually be stored in constant memory, such as string literals. The C++ standard says that, no matter what the case, modifying constant objects has undefined behavior. This may be somewhat confusing to digest, so here are some examples:
int a = 0;
int const &b = a; //b is a const view of the object
a = 1; //this is fine, the change can also be observed through b
int &c = const_cast<int &>(b); //bad
c = 2; //undefined behavior: anything could happen
As with any undefined behavior, anything at all could happen - it could "work", it could have no effect, it could cause the program to crash, or it might summon an angry bear to smash your computer with a hammer. In this case, 99% of the time it will work, since there is no performance benefit to tracking how the object is accessed. However an implementation would be conforming if it did track how the object was accessed and did whatever it wanted.
How about another example?
char const *s = "Hello, world!";
char *s2 = const_cast<char *>(s); //bad
std::cout << s2 << std::endl; //fine, but scary
s2 = 'h'; //undefined behavior, anything could happen
std::cout << s2 << std::endl; //assuming the program is still running, what does this print?
std::cout << s << std::endl; //assuming the program is still running, what does this print?
As can be seen here, changing the first character of the string through
s2 causes a segmentation fault. Getting a segfault is lucky - the program stopped before it got into a corrupt state. If the program had continued running, who knows what would have happened? It would be in a corrupt state. The two outputs after changing the first character might even be different from each other.
When your code is handed a const variable, you can't just ignore the const and side-step it. The fact that you can is considered good by those who disike const-correctness and bad by those who do like const-correctness. Since I am arguing in favor of const-correctness, I'm going to consider
const_cast to be deprecated and therefore irrelevant for the rest of this article. I only mentioned it because I have actually seen other articles where people say they cast away constness all the time! The horror!
So far I haven't settled any doubts - if you were against const-correctness, you still are at this point. I hope to change this now - there is a very good reason for having const-correctness, and Java is a great example.
Huh? Java is a great example of why const-correctness is important? But Java doesn't have const-correctness! That's right, Java doesn't have it, and it suffers as a result. For one, the mutability of an object is determined by its interface. This means you need separate interfaces if you want separate kinds of access (read-write and read-only)! This gets messy fast, and the common solution is to have only the read-only interface for objects. An example of this is Java's String class.
Java's String class stores a string, but does not allow modifications. Instead, you must construct new strings with combinations and substrings of existing strings. What's wrong with this, you ask? That's how some C++ strings are implemented anyway, you ask? You've hit the nail on the head - it's an implementation detail. The Java String class' public interface is affected by its underlying implementation. Guess what? There's a StringBuilder class that is effectively a mutable String. Now you have two interfaces for the same thing.
The problems don't stop there. If no read-only version of a class is provided, and you only have the mutable version, you have to write the wrapper yourself, which is a boring and tedious task of creating an interface that only provides methods which do not mutate the wrapped object. This is prone to code duplication as multiple client libraries each have their own internal wrappers. It also leads to the 'design pattern' of not designing mutable objects - that is, making all your interfaces like the String class.
That might make perfect sense in a functional language, but it makes absolutely no sense in imperative languages, where the focus is almost exclusively on manipulating objects. "But if the focus is on manipulating objects then why are you advocating const-correctness?" Let's switch gears back to C++.
Try to tell me you have never been in a situation where you have handed an object to a function and expected the object's state not to have changed after the function has returned. If you can honestly tell me that you have never been in such a situation, you have written enough code. Huh? You think you have written plenty of code? Show me your code ;)
There are many times in every language where you want to allow other code access to an object but under the promise that that other code will not change the state of the object. This is where const-correctness comes in: the promise becomes a guarantee as the const-ness becomes part of the type of the variable. It is a contract, and if violated, we have that undefined behavior I mentioned above.
void f(Type const &t);
The above function prototype says "I, overload of
f, promise not to directly change the state of my parameter
t". It may be called with a non-const variable, a const-variable, or even a temporary.
There is a catch - notice the lawyer word "directly" in the contract promise - how does this come into play, you ask? Well, unbeknownst to the function, other code may have non-const access to the object, and if the function uses that other code, the state of the object may be changed indirectly by the function. This is can be confusing to the function and confusing to the caller if the fine print is not examined carefully.
So what good is const-correctness if it can cause confusion by being circumvented? Well, this situation shouldn't happen with well-designed code. It's a lazy excuse for sure, but the benefits of const-correctness outweigh the flaws. "What benefits?" Oh, right, the benefits.
For one, the const and non-const interfaces are merged into one; member functions which do not alter the state of the object are marked const and can be invoked from const variables, where as member function that do modify the state of the object are not marked const and cannot be invoked from a const variable. This makes the contract less a promise and more a law of physics - it can't be broken.
The fact that the contract is not a promise and instead a law gives the programmer great confidence that they don't have to worry about their objects' states mysteriously changing. Even with the indirect change of state, the programmer knows exactly which code has write access to the object, and so can easily track how that code is being unexpectedly invoked.
In languages without const-correctness, you have to be aware that all code you give your objects to have the potential to change the state of those objects - I've already used Java to show how this is problematic. Those who dislike const-correctness try to ignore it when they write code, and even forcefully remove it when they use others' code. Then others try to use their code and they're back to doing things the Java way in C++.
That's the problem: people trying to use their practices from language B in language A and expecting it to work. Const-correctness isn't a practice or design pattern, it is a language feature. The fact that is has to be replicated with design patterns in other languages is unacceptable.
"I'm still not convinced..." Maybe you enjoy working in an environment with no guarantees, nothing to prevent things you don't want to happen from happening. Well, writing all your own code is a bad idea - collaboration works best, and reinventing the wheel is wasteful. You can't read minds so what makes you think anyone else can read yours? How should anyone else know not to modify an object you give them, and how should they know that you won't modify objects they give you? With a comment?
A comment is a weak promise. Const-correctness is a contract. It doesn't require writing whole separate interfaces, it just requires you to type five letters.