Item 3: Use const whenever possible


Item 3: Use const whenever possible

The wonderful thing about const is that it allows you to specify a semantic constraint a particular object should not be modified and compilers will enforce that constraint. It allows you to communicate to both compilers and other programmers that a value should remain invariant. Whenever that is true, you should be sure to say so, because that way you enlist your compilers' aid in making sure the constraint isn't violated.

The const keyword is remarkably versatile. Outside of classes, you can use it for constants at global or namespace scope (see Item 2), as well as for objects declared static at file, function, or block scope. Inside classes, you can use it for both static and non-static data members. For pointers, you can specify whether the pointer itself is const, the data it points to is const, both, or neither:

 char greeting[] = "Hello"; char *p = greeting;                    // non-const pointer,                                        // non-const data const char *p = greeting;              // non-const pointer,                                        // const data char * const p = greeting;             // const pointer,                                        // non-const data const char * const p = greeting;       // const pointer,                                        // const data 

This syntax isn't as capricious as it may seem. If the word const appears to the left of the asterisk, what's pointed to is constant; if the word const appears to the right of the asterisk, the pointer itself is constant; if const appears on both sides, both are constant.

When what's pointed to is constant, some programmers list const before the type. Others list it after the type but before the asterisk. There is no difference in meaning, so the following functions take the same parameter type:

 void f1(const Widget *pw);         // f1 takes a pointer to a                                    // constant Widget object void f2(Widget const *pw);         // so does f2 

Because both forms exist in real code, you should accustom yourself to both of them.

STL iterators are modeled on pointers, so an iterator acts much like a T* pointer. Declaring an iterator const is like declaring a pointer const (i.e., declaring a T* const pointer): the iterator isn't allowed to point to something different, but the thing it points to may be modified. If you want an iterator that points to something that can't be modified (i.e., the STL analogue of a const T* pointer), you want a const_iterator:

 std::vector<int> vec; ... const std::vector<int>::iterator iter =     // iter acts like a T* const   vec.begin(); *iter = 10;                                 // OK, changes what iter points to ++iter;                                    // error! iter is const std::vector<int>::const_iterator cIter =   //cIter acts like a const T*   vec.begin(); *cIter = 10;                               // error! *cIter is const ++cIter;                                  // fine, changes cIter 

Some of the most powerful uses of const stem from its application to function declarations. Within a function declaration, const can refer to the function's return value, to individual parameters, and, for member functions, to the function as a whole.

Having a function return a constant value often makes it possible to reduce the incidence of client errors without giving up safety or efficiency. For example, consider the declaration of the operator* function for rational numbers that is explored in Item 24.

 class Rational { ... }; const Rational operator*(const Rational& lhs, const Rational& rhs); 

Many programmers squint when they first see this. Why should the result of operator* be a const object? Because if it weren't, clients would be able to commit atrocities like this:

 Rational a, b, c; ... (a * b) = c;                           // invoke operator= on the                                        // result of a*b! 

I don't know why any programmer would want to make an assignment to the product of two numbers, but I do know that many programmers have tried to do it without wanting to. All it takes is a simple typo (and a type that can be implicitly converted to bool):

 if (a * b = c) ...                       // oops, meant to do a comparison! 

Such code would be flat-out illegal if a and b were of a built-in type. One of the hallmarks of good user-defined types is that they avoid gratuitous incompatibilities with the built-ins (see also Item 18), and allowing assignments to the product of two numbers seems pretty gratuitous to me. Declaring operator*'s return value const prevents it, and that's why it's The Right Thing To Do.

There's nothing particularly new about const parameters they act just like local const objects, and you should use both whenever you can. Unless you need to be able to modify a parameter or local object, be sure to declare it const. It costs you only the effort to type six characters, and it can save you from annoying errors such as the "I meant to type '==' but I accidently typed '='" mistake we just saw.

const Member Functions

The purpose of const on member functions is to identify which member functions may be invoked on const objects. Such member functions are important for two reasons. First, they make the interface of a class easier to understand. It's important to know which functions may modify an object and which may not. Second, they make it possible to work with const objects. That's a critical aspect of writing efficient code, because, as Item 20 explains, one of the fundamental ways to improve a C++ program's performance is to pass objects by reference-to-const. That technique is viable only if there are const member functions with which to manipulate the resulting const-qualified objects.

Many people overlook the fact that member functions differing only in their constness can be overloaded, but this is an important feature of C++. Consider a class for representing a block of text:

 class TextBlock { public:   ...   const char& operator[](std::size_t position) const   // operator[] for   { return text[position]; }                           // const objects   char& operator[](std::size_t position)               // operator[] for   { return text[position]; }                           // non-const objects private:    std::string text; }; 

TextBlock's operator[]s can be used like this:

 TextBlock tb("Hello"); std::cout << tb[0];                   // calls non-const                                             // TextBlock::operator[] const TextBlock ctb("World"); std::cout << ctb[0];                  // calls const TextBlock::operator[] 

Incidentally, const objects most often arise in real programs as a result of being passed by pointer- or reference-to-const. The example of ctb above is artificial. This is more realistic:

 void print(const TextBlock& ctb)       // in this function, ctb is const {   std::cout << ctb[0];                 // calls const TextBlock::operator[]   ... } 

By overloading operator[] and giving the different versions different return types, you can have const and non-const TextBlocks handled differently:

 std::cout << tb[0];                   // fine   reading a                                       // non-const TextBlock tb[0] = 'x';                           // fine   writing a                                       // non-const TextBlock std::cout << ctb[0];                  // fine   reading a                                       // const TextBlock ctb[0] = 'x';                          // error!   writing a                                       // const TextBlock 

Note that the error here has only to do with the return type of the operator[] that is called; the calls to operator[] themselves are all fine. The error arises out of an attempt to make an assignment to a const char&, because that's the return type from the const version of operator[].

Also note that the return type of the non-const operator[] is a reference to a char a char itself would not do. If operator[] did return a simple char, statements like this wouldn't compile:

 tb[0] = 'x'; 

That's because it's never legal to modify the return value of a function that returns a built-in type. Even if it were legal, the fact that C++ returns objects by value (see Item 20) would mean that a copy of tb.text[0] would be modified, not tb.text[0] itself, and that's not the behavior you want.

Let's take a brief time-out for philosophy. What does it mean for a member function to be const? There are two prevailing notions: bitwise constness (also known as physical constness) and logical constness.

The bitwise const camp believes that a member function is const if and only if it doesn't modify any of the object's data members (excluding those that are static), i.e., if it doesn't modify any of the bits inside the object. The nice thing about bitwise constness is that it's easy to detect violations: compilers just look for assignments to data members. In fact, bitwise constness is C++'s definition of constness, and a const member function isn't allowed to modify any of the non-static data members of the object on which it is invoked.

Unfortunately, many member functions that don't act very const pass the bitwise test. In particular, a member function that modifies what a pointer points to frequently doesn't act const. But if only the pointer is in the object, the function is bitwise const, and compilers won't complain. That can lead to counterintuitive behavior. For example, suppose we have a TextBlock-like class that stores its data as a char* instead of a string, because it needs to communicate through a C API that doesn't understand string objects.

 class CTextBlock { public:   ...   char& operator[](std::size_t position) const   // inappropriate (but bitwise   { return pText[position]; }                    // const) declaration of                                                  // operator[] private:   char *pText; }; 

This class (inappropriately) declares operator[] as a const member function, even though that function returns a reference to the object's internal data (a topic treated in depth in Item 28). Set that aside and note that operator[]'s implementation doesn't modify pText in any way. As a result, compilers will happily generate code for operator[]; it is, after all, bitwise const, and that's all compilers check for. But look what it allows to happen:

 const CTextBlock cctb("Hello");        // declare constant object char *pc = &cctb[0];                   // call the const operator[] to get a                                        // pointer to cctb's data *pc = 'J';                              // cctb now has the value "Jello" 

Surely there is something wrong when you create a constant object with a particular value and you invoke only const member functions on it, yet you still change its value!

This leads to the notion of logical constness. Adherents to this philosophy argue that a const member function might modify some of the bits in the object on which it's invoked, but only in ways that clients cannot detect. For example, your CTextBlock class might want to cache the length of the textblock whenever it's requested:

 class CTextBlock { public:   ...   std::size_t length() const; private:   char *pText;   std::size_t textLength;            // last calculated length of textblock   bool lengthIsValid;                // whether length is currently valid }; std::size_t CTextBlock::length() const {   if (!lengthIsValid) {     textLength = std::strlen(pText);  // error! can't assign to textLength     lengthIsValid = true;             // and lengthIsValid in a const   }                                   // member function   return textLength; } 

This implementation of length is certainly not bitwise const both textLength and lengthIsValid may be modified yet it seems as though it should be valid for const CTextBlock objects. Compilers disagree. They insist on bitwise constness. What to do?

The solution is simple: take advantage of C++'s const-related wiggle room known as mutable. mutable frees non-static data members from the constraints of bitwise constness:

 class CTextBlock { public:   ...   std::size_t length() const; private:   char *pText;   mutable std::size_t textLength;         // these data members may   mutable bool lengthIsValid;             // always be modified, even in };                                        // const member functions std::size_t CTextBlock::length() const {   if (!lengthIsValid) {     textLength = std::strlen(pText);      // now fine     lengthIsValid = true;                 // also fine   }   return textLength; } 

Avoiding Duplication in const and Non-const Member Functions

mutable is a nice solution to the bitwise-constness-is-not-what-I-had-in-mind problem, but it doesn't solve all const-related difficulties. For example, suppose that operator[] in TextBlock (and CTextBlock) not only returned a reference to the appropriate character, it also performed bounds checking, logged access information, maybe even did data integrity validation. Putting all this in both the const and the non-const operator[] functions (and not fretting that we now have implicitly inline functions of nontrivial length see Item 30) yields this kind of monstrosity:

 class TextBlock { public:   ...   const char& operator[](std::size_t position) const   {     ...                                 // do bounds checking     ...                                 // log access data     ...                                 // verify data integrity     return text[position];   }   char& operator[](std::size_t position)   {     ...                                 // do bounds checking     ...                                 // log access data     ...                                 // verify data integrity     return text[position];   } private:    std::string text; }; 

Ouch! Can you say code duplication, along with its attendant compilation time, maintenance, and code-bloat headaches? Sure, it's possible to move all the code for bounds checking, etc. into a separate member function (private, naturally) that both versions of operator[] call, but you've still got the duplicated calls to that function and you've still got the duplicated return statement code.

What you really want to do is implement operator[] functionality once and use it twice. That is, you want to have one version of operator[] call the other one. And that brings us to casting away constness.

As a general rule, casting is such a bad idea, I've devoted an entire Item to telling you not to do it (Item 27), but code duplication is no picnic, either. In this case, the const version of operator[] does exactly what the non-const version does, it just has a const-qualified return type. Casting away the const on the return value is safe, in this case, because whoever called the non-const operator[] must have had a non-const object in the first place. Otherwise they couldn't have called a non-const function. So having the non-const operator[] call the const version is a safe way to avoid code duplication, even though it requires a cast. Here's the code, but it may be clearer after you read the explanation that follows:

 class TextBlock { public:   ...   const char& operator[](std::size_t position) const     // same as before   {     ...     ...     ...     return text[position];   }   char& operator[](std::size_t position)         // now just calls const op[]   {     return       const_cast<char&>(                         // cast away const on                                                  // op[]'s return type;         static_cast<const TextBlock&>(*this)     // add const to *this's type;           [position]                            // call const version of op[]       );   } ... }; 

As you can see, the code has two casts, not one. We want the non-const operator[] to call the const one, but if, inside the non-const operator[], we just call operator[], we'll recursively call ourselves. That's only entertaining the first million or so times. To avoid infinite recursion, we have to specify that we want to call the const operator[], but there's no direct way to do that. Instead, we cast *this from its native type of TextBlock& to const TextBlock&. Yes, we use a cast to add const! So we have two casts: one to add const to *this (so that our call to operator[] will call the const version), the second to remove the const from the const operator[]'s return value.

The cast that adds const is just forcing a safe conversion (from a non-const object to a const one), so we use a static_cast for that. The one that removes const can be accomplished only via a const_cast, so we don't really have a choice there. (Technically, we do. A C-style cast would also work, but, as I explain in Item 27, such casts are rarely the right choice. If you're unfamiliar with static_cast or const_cast, Item 27 contains an overview.)

On top of everything else, we're calling an operator in this example, so the syntax is a little strange. The result may not win any beauty contests, but it has the desired effect of avoiding code duplication by implementing the non-const version of operator[] in terms of the const version. Whether achieving that goal is worth the ungainly syntax is something only you can determine, but the technique of implementing a non-const member function in terms of its const twin is definitely worth knowing.

Even more worth knowing is that trying to do things the other way around avoiding duplication by having the const version call the non-const version is not something you want to do. Remember, a const member function promises never to change the logical state of its object, but a non-const member function makes no such promise. If you were to call a non-const function from a const one, you'd run the risk that the object you'd promised not to modify would be changed. That's why having a const member function call a non-const one is wrong: the object could be changed. In fact, to get the code to compile, you'd have to use a const_cast to get rid of the const on *this, a clear sign of trouble. The reverse calling sequence the one we used above is safe: the non-const member function can do whatever it wants with an object, so calling a const member function imposes no risk. That's why a static_cast works on *this in that case: there's no const-related danger.

As I noted at the beginning of this Item, const is a wonderful thing. On pointers and iterators; on the objects referred to by pointers, iterators, and references; on function parameters and return types; on local variables; and on member functions, const is a powerful ally. Use it whenever you can. You'll be glad you did.

Things to Remember

  • Declaring something const helps compilers detect usage errors. const can be applied to objects at any scope, to function parameters and return types, and to member functions as a whole.

  • Compilers enforce bitwise constness, but you should program using conceptual constness.

  • When const and non-const member functions have essentially identical implementations, code duplication can be avoided by having the non-const version call the const version.




Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
Effective C++ Third Edition 55 Specific Ways to Improve Your Programs and Designs
ISBN: 321334876
EAN: N/A
Year: 2006
Pages: 102

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net