Tuesday, July 20, 2010

When CONSTants Vary

/*

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

  1. 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.
  2. Misnamed because fields are generally mutable even without the keyword. But I suspect the committee would balk at "mutable_even_under_const."

*/

6 comments:

Anonymous said...

Is it not worth it's documentation function?

Even if the compiler can't enforce it in all cases, applying the const modifier documents the intent of maintaining a certain reference's state across a function call.

Anonymous said...

"const" sure has weaknesses, but to me it is an original and interesting experiment. What is sad is that no language tried to generalize further the mecanism. The idea of const is to offer to the programmer the possibility to prove things statically on a plane that is totally orthogonal to the (more mundane) rest of type system. Brilliant ! There are the classic data types and classes and stuff trying to prove safety, and then suddenly you have that arbitrary boolean property: constness. What if a langage had more? Let's call them "user-defined static object states". const can be seen as a particular, predefined case of a boolean state with associated default compiler rules that check mutability. So, in a language with such a generalization, the programmer would define, say, a "color" static state as { red, blue, yellow } and then annotate function calls as valid only if the argument is statically yellow...
Admittedly I don't know at all what I would prove with such an elaborate machinery (other than object mutability), but I liked imagining it :)

Anonymous said...

The problems with const are numerous but seeing this article while I'm migrating to my new favorite language (Scala), I'm pretty blown away by the similarity to Scala's idea of immutability, which every Scala programmer learns on day one doesn't mean an object won't change. Yet we rely on it all the time for the awesome multithreading capabilities that come from functional programming in an immutable context. That should be just enough programming language power to detonate Greenland...

But, just like with const, this hasn't happened to me. Granted I've only had a year with Scala but the 20 I'm sporting with C++ (and 10 with final in Java) give me some confidence. I've definitely seen const do inspiring feats of good so I think its another 'use with caution' deals that is the reason we (don't) make the big bucks.

Or maybe I just don't want to have to learn Clojure.

Anonymous said...

This post is desperately lacking some examples from other languages -- this C++ stuff is just boring (for me).

JamesIry said...

test

Edaqa Mortoray said...

This is a good overview of the problems with "const" in C++. I would of course rather see that new languages fix these problems rather than just discarding "const" entirely. I find that "const" is a really helpful feature in code maintenance.