ACCU Home page ACCU Conference Page ACCU 2017 Conference Registration Page
Search Contact us ACCU at Flickr ACCU at GitHib ACCU at Google+ ACCU at Facebook ACCU at Linked-in ACCU at Twitter Skip Navigation

pinOverloading on const is wrong

Overload Journal #6 - Mar 1995 + Programming Topics   Author: George Wendle

As far as I can see from the current version of the Working Paper on C++ the use of const in C++ programming is getting increasingly perverted. Let me try to explain with the hope that those responsible will reconsider.

The origins of const

As I understand it const first saw the light of day as a mechanism to support passing by reference in a C++ environment. In the days of pure C, parameters were either actual values or they were pointers. Putting aside the argument as to whether passing an address to a pointer is or is not a form of value passing, we clearly had two distinct choices. We could pass a value and the receiving function could do anything it liked with it without damaging the original - rather like sending someone a fax. Or we could pass a an address into a pointer and thereby allow the original to be changed.

Apart from the eccentric form for providing arrays in C this method always allowed the programmer to know if the information made available to the function was 'read only' or not. C accepted that this mechanism was inappropriate for large objects but simply tolerated the problem. Handling arrays, unions and structs was not part and parcel of much of the early programming done in C. It was special enough so that most programmers learnt the special conditions and took suitable care - well at least some did.

Almost immediately in C++ this dichotomy between 'read only' and 'writable' parameters became inadequate. C++ needed true references, if for no other reason than to make user defined types as easy to use as the built in ones. It was immediately obvious that any reference mechanism would only be acceptable to the majority of programmers if they could control access for modification. const was invented. Once C saw the idea it realised that it too could use const to support passing access to large data elements. It developed the idea a little, added volatile and that was it. Francis claims that C got it wrong because const does not make values available at compile time. Actually I think C got it right, const was meant to be a form of access control - you can look but not touch.

C++ develops const

You do not need much experience of C to realise that quite a lot of conversion work can go on fixing the types of arguments to match the types of the parameters they are passed to.

Remembering that const was introduced (or so I believe) to allow references to do the same work as passing by value we have the problem of fixing up type conversions between arguments and the const references being bound to them. Lets look at a snippet of code:

void fn(int i);
main(){
  float f;
  fn(f);
  return 0;
}

We know that value in f will be quietly converted to an int when the call to fn() is made.

Now how about:

void fnp(const int * ip);
main(){
  float f;
  fnp(&f);
  return 0;
}

And that should fail, because we cannot convert a float address to an int*. Of course we can shoot our code in the foot with a cast but that is a different issue.

So now move to C++

void fnr(const int & ir);
main(){
  float f;
  fnr(f);
  return 0;
}

Should this code compile? Well yes, because our intention when we introduced const reference parameters was to allow a form of reference use that would be just as safe as pass by value. As long as the reference was only read and not written it should be impossible to distinguish. There is a hidden consequence, the compiler has to be able to create a temporary object of the required type to bind to the reference, but otherwise fine.

Without the const qualification of the reference parameter the compiler will reject the above code just as it would reject the pointer equivalent because it is probably an error to write to a temporary object.

Note that this actually makes const essential and not just an optional extra. If we remove all uses of const from our code we are going to find that some of our code will break because the compiler will not allow a conversion of an argument type to match a reference parameter type.

The water gets murky

Unfortunately, language designers suffer from the problem of generalising from the sane to the plain ridiculous. If the compiler can make the above conversion for the purpose of passing an argument to a reference parameter why stop there? Why not allow:

float pi=3.1415926;
const int & ipi=pi;

They had already discovered that allowing references in contexts other than parameter passing could be useful. For example:

int array[101];
int & median = array[50];

allowed naming individual elements of an array. Seduced by this latter convenience they conclude that they will have to allow the former insanity. If it got no worse than this, I guess we could live with it. It isn't any more eccentric than many other elements of both C and C++. However stay with me because it is about to get much worse.

Add some real pollution

For completely different reasons C++ introduced function overloading. Bjarne Stroustrup tells us in his excellent book - The Design and Evolution of C++ - that this feature was introduced to support the need for alternative constructors. I am not convinced because I can think of a couple of other viable methods but it certainly is a fairly elegant solution to the problem of providing multiple constructors.

Of course once we have function overloading for constructors, what language designer could resist generalising them? It is interesting to note that this wasn't quite a clear cut issue because in the early days you had to explicitly specify overloading with the keyword overload.

Now we have to set up rules to resolve overloading when some or all parameters have to be converted. Clearly

void fn(int i);
void fn(const int & i);

should not be distinguishable because the whole motive for allowing const reference parameters was to provide an alternative mechanism to passing by value. But what about:

void fn(int & i);
void fn(const int & i);

Think about it. Ask yourself if you can get any benefit from such a set of overloaded functions that you could not get from:

void fn(int & i);
void fnconst(const int & i);

Well in the second case you will always have to say what you mean. I think that is rather an advantage. Consider what a compiler would do with the following code in each of the two cases above.

main(){
  int i;
  const j=i;
  float f=2.4;
  fn(i);
  fn(j);  // error in case 2
  fn(f);  // error in case 2
  return 0;
}

Do the two errors that the second case would generate do anything but improve the quality of the consequential code?

Unfortunately we do not always have the option of using a different function name. Operators are rather attractive and we certainly want to overload those. There is only one spelling for any operator function and so all differences must be handled via the overload mechanism. Consider the following:

class Array {
  int array[100];
  ... // other details
public:
  int & operator [](int);
  const int & operator [](int)const;
  ... // other details
};

Why two operator []()? Think about it. An explicit Array object might be a const object (initialised by an appropriate constructor but that is another problem). Even then we would want to be able to write something like:

cout<<ao[10];

None-the-less, most of our array objects would be non-const and so we would like to write code like:

ao[10]=5;

If we are going to have it both ways we need to overload operator[]() on const.

Now my tale is complete. At each step we have done things that are reasonable or attractive but the end result is mad to an extent that most programmers will not even know which of two functions will be called in each case. Note that the compiler does not need to provide a temporary when converting to a base class because the object really is an object of that type so:

void fn (D &);
void fn (const D &);
main (){
  B b;
  fn (b);
  return 0;
}

And now to decide which fn() is called, if any, you need to know whether B is a base class of D or if B provides an explicit conversion to D or neither.

However seductive the rationale for allowing overload resolution based on const it is a poisoned gift, one we would be much better off without.

const was and is a form of access control, for excellent reasons we do not allow overload resolution based on other forms of access and we should not do so for const.

Please, please, remove this excrescence from the language now before it is too late. I do not believe that the convenience of overloaded read-only and writable versions of operator[]() are worth the cost. And apart from operators, I can get the functionality without overloading on const.

Overload Journal #6 - Mar 1995 + Programming Topics