/*
Occasionally somebody will suggest that statically typed language X (Java, C#, Scala, F#, whatever) would be better off with C++'s "const", a type qualifer that supposedly makes objects immutable in certain contexts[1]. I'm not convinced. Const requires a fair amount of work and provides fairly small guarantees. This post is a bit of literate C++ program demonstrating some of the weaknesses in const.
There are a couple of holes that I want to dismiss quickly. First there's the weakness that const can be cast away. But hey, the C and C++ motto is "casting away safety since 1972." Blowing your leg off with a cast is half the fun of using the C derived languages. Second, there's the useful, if slightly misnamed, weakness of the "mutable"[2] keyword which can be applied to a field and says "this field can be mutated even in const contexts." Mutable is useful for things like memoizing.
But even ignoring those very deliberate issues, there are still a couple of fundamental weaknesses in how C++ deals with const.
For my example I'm going to use a simple toy class, a Counter that can be incremented (mutating its state) and then queried to see what the current count is.
*/
#include <iostream>
using namespace std;
class Counter {
int _count;
public:
Counter() : _count(0) {}
void inc() {
_count++;
}
/*
A perfectly good use of const. The count() query doesn't change the state of this Counter.
*/
int count() const{
return _count;
}
};
/*
Indirection
So far, so good. But we can easily destroy const with a bit of indirection. Imagine I need something that holds a pair of Counters and, for whatever reason, I need to do so via pointers.
*/
class CounterPair {
Counter *_c1;
Counter *_c2;
public:
CounterPair() {
_c1 = new Counter();
try {
_c2 = new Counter();
} catch(...) {
delete _c1;
throw;
}
}
// technically should have copy constructor
// and operator=, but I'm too lazy
~CounterPair() {
delete _c1;
delete _c2;
}
/*
These two methods query the state of the CounterPair and are safely const
*/
int count1() const {
return _c1 -> count();
}
int count2() const {
return _c2 -> count();
}
/*
Up to this point everything is golden. But these next two methods modify the state of the CounterPair, yet are marked const.
*/
void inc1() const {
_c1 -> inc();
}
void inc2() const {
_c2 -> inc();
}
};
/*
What gives? Well, C++ doesn't appear to understand the that state and memory are two different things. I didn't modify the memory that lies directly in one CounterPair envelope but I'm clearly modifying the state of the CounterPair. "Transitive constness" is one area where the D programming language does much better than C++.
Aliasing
An even more subtle issue happens with aliasing. The following function does not modify its first argument but does modify its second. The first is marked const.
*/
void incSecondCounter(Counter const &c1, Counter &c2) {
c2.inc();
}
int main(int argc, char** argv) {
Counter c;
/*
But if we create a bit of aliasing then c1's referent gets modified
*/
Counter const &c1 = c; Counter &c2 = c; cout << c1.count() << endl; incSecondCounter(c1, c2); cout << c1.count() << endl; }
/*
In this toy program that looks silly, but in larger programs aliasing can be quite subtle. And aliasing is a tricky, tricky problem to deal with well at the type level so even the D language will have an equivalent hole in const. At best what const says here is that "the object won't be modified via that particular reference," which is a heck of a lot weaker than "the object won't be modified."
Conclusion
Const correctness requires a lot of typing of both the push-the-buttons-on-the-keyboard kind and the keep-certain-static-properties-consistent variety. Yet it gives back relatively little. Even without poking holes in const using casting or "mutable" it's easy to get mutation in something that's supposed to be immutable. C++'s variety of const doesn't deal with indirection at all well and aliasing is a tricky nut that would substantially alter a language's type system. Is const worth adding to other languages? I'm not sure.
Footnotes
- C'99 also has a const keyword with very similar semantics. So if you replace references with pointers and strip out the thin veneer of OO this article would cover C as well.
- Misnamed because fields are generally mutable even without the keyword. But I suspect the committee would balk at "mutable_even_under_const."
*/