Previous | Table of Contents | Next |
8.3.1.2. Sneaky Functions
Let us now consider a function that lies about what it is going to do to its argument:
void clobber(const int* p) { int* q = (int*) p; *q = 0; }
This function takes a pointer to memory that we have promised not to modify. It then goes ahead and uses that pointer to modify the memory. Can it do that?
In general, of course, the answer is no. For example, the effect of the following is undefined:
const int x = 42; clobber(&x);
Here, &x points to memory that might be write-protected, so the moment clobber executes the cast (int*)p, the effect of anything after that is undefined. But consider the following:
int y = 42; clobber(&y);
Here, everything is legal! The value of &y is a pointer to modifiable memory. Calling clobber converts that pointer into a const int*, and inside clobber, that pointer is converted back to an int*. Our second rule says that this conversion is safe. In other words, this use of clobber is legitimately defined to set y to 0. The language does not prevent the function from violating its promise not to change y.
8.3.1.3. Theory and Practice
In theory, then, attaching a const to a pointer argument doesnt guarantee much. In practice, however, it is hard to write a function such as clobber that does anything useful.
The reason lies in the first example of using clobber:
const int x = 42; clobber(&x);
Here, there is no question that the use of clobber violates the language rules, whether the compiler checks it or not. That means that the author of clobber has no legitimate reason to write it that way. In effect, when clobber violates its promise, that makes the user of clobber responsible for verifying that the program is never called with a const object as its argument. The author of clobber gains nothing significant, and the user loses.
8.3.1.4. const Promises Summary
A function that takes a pointer to const as an argument makes a promise to its users that it will not use that pointer to modify the memory that the pointer addresses. C++ offers the possibility of writing functions that accept such constant pointers and then modify the memory anyway.
On the surface, it might seem that such an ability is useless. However, it is a by-product of the desire to be able to have a single type that is able to store a pointer to memory that might, or might not, be modifiable. We did just that, for example, when we collapsed the two versions of strchr into one. To write such a function, however, one must be able to convert both const and non-const pointers to a single type, provided only that one remembers, by other means, the original type of a pointer. This ability gives rise to the rule that any pointer can be converted to a pointer to const and back to its original type with impunity.
On the surface, therefore, it might appear that it is meaningless for a function ever to promise that it will not change the memory at which one of its arguments points. In practice, however, a function that breaks such a promise runs afoul of the language definition if it is ever called with a pointer to genuinely constant memory. Such behavior can therefore rightfully be considered cheating. The way to avoid the temptation to cheat that way is to examine every cast that takes away a const from a pointer type and prove that the pointer must always point to memory that the program has permission to modify.
A reference is a way of attaching a name to an object. For example, after
int x = 42; int& y = x;
the names x and y refer to the same object; the second declaration attaches y to the object denoted by x.
Often, the reason for giving an object a name is that it doesnt have one:
int& elem = a[i];
attaches the name elem to element i of array a. One might do this, for example, if one were about to use a[i] several times. Using a reference saves program text and might save computation time as well.
Although references usually have names directly associated with them, there is one common exception: Functions may return references. Here, for example, is a convoluted way of setting x to 0:
int x; int& f() { return x; } main() { f() = 0; // x = 0 }
Here the function f returns a reference to the global variable x; assigning a value to f() is therefore equivalent to assigning the value to x.
There are three common reasons for functions to return references.
One is for assignment operators:
class Thing { public: // ... Thing& operator=(const Thing&); // ... }; Thing& operator=(const Thing& t) { // ... return *this; }
The usual practice is for such operators to return *this as a reference. This practice makes it possible to chain assignments:
Thing t1, t2, t3; // ... t1 = t2 = t3;
Of course, such chaining is also possible if operator= returns a Thing instead of a Thing&:
class Thing { public: // ... Thing operator=(const Thing&); // ... }; Thing operator=)const Thing& t) { // ... return *this; }
but doing this is not a good idea: Without fairly clever compiler optimization, such an assignment operator always forms a copy of the object being assigned, even though that copy is usually thrown away.
The second common use for functions that return references is to allow member function calls to be chained easily. Consider, for example, a class Text whose objects represent strings of text. Such objects might have an associated font, size, and so on; the class might look something like this:
class Text { public: Text(const String&); Text& setsize(int); Text& setfont(const String&); // ... };
If the setsize and setfont members each return *this as a reference, we can then write things such as
Text t(The quick brown fox); t.setsize(12).setfont(Goudy);
Previous | Table of Contents | Next |