Previous Table of Contents Next


8.3. Semantic Pitfalls

8.3.1. How Much Does const Promise?

Consider a function declared as

   extern void f(const char*);

If we call f with the address of a char object c, as in

   char c = ‘ ‘;
   f(&c);

we are assured that c still has the value that it did before the call. Right?

The answer turns out to be “yes and no” for complicated reasons. To understand the reasons, we will first look at when it is legitimate to convert a pointer to const into an ordinary pointer. After that, we will be able to understand why such conversions might make it possible for a function such as f to break its promise not to change c. Finally, we will discuss how common such broken promises are likely to be in practice.

8.3.1.1. Casting Away const

The well-known C library function strchr opens a hole in the type system. The function takes two arguments: a pointer to the initial character of a null-terminated string and a character to seek in the string. If strchr finds the character, it returns a pointer to the first instance of the character; otherwise it returns 0.

The hole becomes easier to notice when we ask what should logically be the argument and return types of strchr. Consider the argument type first. It is clearly useful to be able to find a character in an array that we have promised not to modify. That implies that the first argument to strchr should be a pointer to const. Otherwise, we would not be allowed to declare a table that we promise not to change by saying something like

   const char table[] = { /* ... */ };

and then to search it by calling strchr(table, c).

On the other hand, a similar line of reasoning argues that the result of strchr should not be a pointer to const. Otherwise, we cannot use strchr to locate a character in a modifiable character array that we want to modify.

In C, then, the strchr function ought logically to be declared like this:

   char *strchr(const char *, char);

and indeed, this is how the C library defines it. But this definition implies that we can give strchr a pointer to const and get back a pointer to the same data structure that has no restrictions against modifying the data to which it points.

C++ closes this loophole by overloading strchr:

   char* strchr(char*, char);
   const char* strchr(const char*, char);

Now, if you call strchr with a char* argument, the result is also char*, and if you call strchr with a const char* argument, the result won’t let you modify the underlying memory either. The loophole has been closed.

After understanding the strchr type loophole, we can turn our attention from the definition of strchr to its implementation. We have two versions to implement—one with const and the other without it—so it is natural to try to implement one in terms of the other. Suppose we implement the char* version first:

   char* strchr(char* p, char x)
   {
        while (*p != x) {
             if (*p == ‘\0’)
                   return 0;
             ++p
        }
        return p;
   }

If we now set out to implement the const char* version, we discover that indeed the two versions are identical except for the argument and result types. We might expect, therefore, that it should be trivial to implement one of them in terms of the other. Moreover, inline functions should let us do that with no overhead at execution time.

To implement one version of strchr in terms of the other, however, we must decide what will be the type of p in the version that we implement fully. Suppose, for example, that we try to implement the const version of strchr in terms of the one we already have:

   // not quite right
   const char* strchr(const char* p, char x)
   {
        return ::strchr((char*) p, x);
   }

It should be clear that the use of (char*) p in the argument to ::strchr is dangerous indeed, at least from the viewpoint of the language. After all, it is possible that p might point to memory that is write protected in hardware. From a pedantic viewpoint, the conversion of p to char* is illegal.

The foregoing discussion implies that if we want to implement one version of strchr in terms of the other, we should do it this way:

   const char* strchr(const char* p, char x)
   {
        while (*p != x) {
             if (*p == ‘\0’)
                   return 0;
             ++p;
        }
        return p;
   }
   char* strchr(char* p, char x)
   {
        return (char*) ::strchr((const char*)p, x);
   }

Here, the cast of the argument of ::strchr is safe because converting from char* to const char* is safe. We need to cast only to ensure that the call to ::strchr will select the appropriate overloaded version of strchr and not yield a recursion loop. On the other hand, we do need a cast to convert the result back to char*. Fortunately, this cast is also safe—because we execute it only when p is pointing to memory that we have permission to modify.

We can sum up the foregoing discussion by writing two rules:

  Converting a plain pointer into a pointer to const is always safe.
  Converting a pointer to const into a plain pointer is legal only when the pointer points to memory that was not const to begin with.


Previous Table of Contents Next