Chapter 14. Const Correctness

FAQ 14.01 How should pointer declarations be read?

Pointer declarations should be read right to left.

If Fred is some type, then

Fred* is a pointer to a Fred (the * is pronounced “pointer to a”).

const Fred* is a pointer to a Fred that cannot be changed via that pointer.

Fred* const is a const pointer to a Fred. The Fred object can be changed via the pointer, but the pointer itself cannot be changed.

const Fred* const is a const pointer to a Fred that cannot be changed via that pointer.

References are similar: read them right to left.

Fred& is a reference to a Fred (the & is pronounced “reference to a”).

const Fred& is a reference to a Fred that cannot be changed via that reference.

Note that Fred& const and const Fred& const are not included in the second list. This is because references are inherently immutable: you can never rebind the reference so that it refers to a different object.

FAQ 14.02 How can C++ programmers avoid making unexpected changes to objects?

With proper use of the keyword const, the C++ compiler detects many unexpected changes to objects and flags these violations with error messages at compile time. This is often called const correctness. For example, function f() uses the const keyword to restrict itself so that it won't be able to change the caller's string object:

image

If f() changes its passed string anyway, the compiler flags it as an error at compile time:

image

In contrast, function g() declares its intent to change the caller's string object by its lack of the const keyword in the appropriate place:

image

For example, it is legal and appropriate for g() to modify the caller's string object:

image

Also it would be legal and appropriate for g() to pass its parameter to f(), since the called function, f(), is at least as restrictive as the caller, g() (in this case, the called function is actually more restrictive):

image

However, it would be illegal for the opposite to occur. That is, if f() passed its parameter to g(), the compiler would give an error message since the called function, g(), is less restrictive than the caller, f():

image

FAQ 14.03 Does const imply runtime overhead?

No, there is no runtime overhead. All tests for constness are done at compile time. Neither runtime space nor speed is degraded.

FAQ 14.04 Does const allow the compiler to generate more efficient code?

Occasionally, but that's not the purpose of const. The purpose of const is correctness, not optimization. That is, const helps the compiler find bugs, but it does not (normally) help the compiler generate more efficient code.

Declaring the constness of a parameter or variable is just another form of type safety; therefore, const correctness can be considered an extension of C++'s type system. Type safety provides some degree of semantic integrity by promising that, for instance, something declared as a string cannot be used as an int. However, const correctness guarantees even tighter semantic correctness by making sure that data that is not intended to be changed cannot be changed. With const correctness, it is easier to reason about the correctness of the software. This is helpful during software inspection.

It is almost as if const string and string are of different, but related, classes. Because type safety helps produce correct software (especially in large systems and applications), const correctness is a worthy goal.

FAQ 14.05 Is const correctness tedious?

It is no more tedious than declaring the type of a variable.

In C++, const correctness is simply another form of type information. In theory, expressing any type information is unnecessary, given enough programmer discipline and testing. In practice, developers often leave a lot of interesting information about their code in their heads where it cannot be exploited or verified by the compiler. For instance, when programmers write a function such as the following print() function, they know implicitly that they are passing by reference merely to avoid the overhead of passing by value; there is no intention of changing the string during the print() operation.

image

If this information is documented only by comments in the code or in a separate manual, it is easy for these comments to become inconsistent with the code; the compiler can't read comments or manuals. The most cost-effective way to document this information is with the five-letter word const:

image

This form of documentation is succinct, in one place, and can be verified and exploited by the compiler.

FAQ 14.06 Why should const correctness be done sooner rather than later?

Adding constraints later can be difficult and expensive. If a function was not originally restricted with respect to changing a by-reference parameter, adding the restriction (that is, changing a parameter from string& to const string&) can cause a ripple through the system. For instance, suppose f() calls g(), and g() calls h():

image

Changing f(string& s) to f(const string& s) causes error messages until g(string&) is changed to g(const string&). But this change causes error messages until h(string&) is changed to h(const string&), and so on. The ripple effect is magnificent—and expensive.

The moral is that const correctness should be installed from the very beginning.

FAQ 14.07 What's the difference between an inspector and a mutator?

An inspector is a member function that returns information about an object's state without changing the object's abstract state (that is, calling an inspector does not cause an observable change in the behavior of any of the object's member functions). A mutator changes the state of an object in a way that is observable to outsiders: it changes the object's abstract state. Here is an example.

image

The pop() member function is a mutator because it changes the Stack by removing the top element. The numElems() member function is an inspector because it simply counts the number of elements in the Stack without making any observable changes to the Stack. The const decoration after numElems() indicates that numElems() promises not to change the Stack object.

Only inspectors may be called on a reference-to-const or pointer-to-const:

image

FAQ 14.08 When should a member function be declared as const?

image

There are two ways to look at it. When looking at the member function from the inside out, the answer is “whenever the member function wants to guarantee that it won't make any observable changes to its this object.” When looking at the member function from the outside in, the answer is “whenever a caller needs to invoke the member function via a reference-to-const or pointer-to-const.” Hopefully these two approaches end up agreeing with each other. If not, then the application may have a serious design flaw, or perhaps it needs to be const overloaded (that is, two member functions with the same name and the same parameters, but one is a const member function and the other is not).

The compiler won't allow a const member function to change *this or to invoke a non-const member function for this object:

image

Although not fleshed out in this example, member functions push() and pop() may throw exceptions in certain circumstances. In this case they throw runtime_error, which is the standard exception class that is thrown for errors that are detectable only at runtime.

FAQ 14.09 Does const apply to the object's bitwise state or its abstract state?

The const keyword should refer to the object's abstract state.

The const modifier is a part of the class's public: interface; therefore, it means what the designer of the public: interface wants it to mean. As an interface designer, the most useful strategy is to tie const to the object's abstract state rather than to its bitwise state. For example, in some circumstances a member function changes its object's bitwise state, yet the change doesn't cause any observable change to any of the object's public: member functions (that is, the abstract state is not changed). In this case, the member function should still be const since it never changes the meaning of the object (see FAQ 14.12). It is even more common for a member function to change an object's abstract state even though it doesn't change the object's bitwise state.

For example, the following MyString class stores its string data on the heap, pointed to by the member datum data_. (The name bad_alloc is the standard exception class that is thrown when memory is exhausted, and the name out_of_range is the standard exception class that is thrown when a parameter is out of range.)

image

The abstract state of the MyString object s is represented by values returned by s[i], where i ranges from 0 to s.size()–1, inclusive. The bitwise state of a MyString is represented by the bits of s itself (that is, by s.len_ and the pointer s.data_).

Even though s.toUpper() doesn't change s.len_ or the pointer s.data_, MyString::toUpper() is a non-const member function because it changes the abstract state (the state from the user's perspective). In other words, toUpper() doesn't change the bitwise state of the object, but it does change the meaning of the object; therefore it is a non-const member function.

FAQ 14.10 When should const not be used in declaring formal parameters?

Do not use const for formal parameter types that are passed by value, because a const on a pass-by-value parameter affects (constrains) only the code inside the function; it does not affect the caller. For example, replace f(const Fred x) with either f(const Fred& x) or f(Fred x).

As a special case of this rule, it is inappropriate to use Fred* const in a formal parameter list. For example, replace f(Fred* const p) with f(Fred* p), and replace g(const Fred* const p) with g(const Fred* p).

Finally, do not use Fred& const in any context. The construct is nonsensical because a reference can never be rebound to a different object. (See FAQ 14.01.)

FAQ 14.11 When should const not be used in declaring a function return type?

A function that returns its result by value should generally avoid const in the return type. For example, replace const Fred f() with either Fred f() or const Fred& f(). Using const Fred f() can be confusing to users, especially in the idiomatic case of copying the return result into a local.

The exception to this rule is when users apply a const-overloaded member function directly to the temporary returned from the function. An example follows.

image

Because f() returns a non-const Fred, f().wilma() invokes the non-const version of Fred::wilma(). In contrast, g() returns a const Fred, so g().wilma() invokes the const version of Fred::wilma(). Thus, the output of this program is as follows.

f(): Fred::wilma()
g(): Fred::wilma() const

FAQ 14.12 How can a “nonobservable” data member be updated within a const member function?

image

Preferably the data member should be declared with the mutable keyword. If that cannot be done, const_cast can be used.

A small percentage of inspectors need to make nonobservable changes to data members. For example, an object whose storage is physically located in a database might want to cache its last lookup in hopes of improving the performance of its next lookup. In this case there are two critical issues: (1) if someone changes the database, the cached value must somehow be either changed or marked as invalid (cache coherency); (2) the const lookup member function needs to make a change to the cached value. In cases like this, changes to the cache are not observable to users of the object (the object does not change its abstract state; see FAQ 14.09).

The easiest way to implement a nonobservable change is to declare the cache using the mutable keyword. The mutable keyword tells the compiler that const member functions are allowed to change the data member.

image

The second alternative is to cast away the constness of the this pointer using the const_cast keyword. In the following example, self is equal to this (that is, they point to the same object), but self is a Fred* rather than a const Fred* so self can be used to modify the this object.

image

FAQ 14.13 Can an object legally be changed even though there is a const reference (pointer) to it?

image

Yes, due to aliasing.

The const part restricts the reference (pointer); it does not restrict the object. Many programmers erroneously think that the object on the other end of a const reference (pointer) cannot change. For example, if const int& i refers to the same int as int& j, j can change the int even though i cannot. This is called aliasing, and it can confuse programmers who are unaware of it.

image

There is no rule in C++ that prohibits this sort of thing. In fact, it is considered a feature of the language that programmers can have several pointers or references refer to the same object (plus it could not be figured out in some cases, e.g., if there are intermediate functions between main() and sample(const int&,int&) and if these functions are defined in different source files and are compiled on different days of the week). The fact that one of those references or pointers is restricted from changing the underlying object is a restriction on the reference (or pointer), not on the object.

FAQ 14.14 Does const_cast mean lost optimization opportunities?

image

No, the compiler doesn't lose optimization opportunities because of const_cast.

Some programmers are afraid to use const_cast because they're concerned that it will take away the compiler's ability to optimize the code. For example, if the compiler cached data members of an object in registers, then called a const member function, in theory it would need to reload only those registers that represent mutable data members. However in practice this kind of optimization cannot occur, with or without const_cast.

The reason the optimization cannot occur is that it would require the compiler to prove that there are no non-const references or pointers that point to the object (the aliasing problem; see FAQ 14.13), and in many cases this cannot be proved.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset