More Books
C++ Gotchas: Avoiding Common Problems in Coding and Design
Main Page
Table of content
Copyright
Addison-Wesley Professional Computing Series
Preface
Acknowledgments
Chapter 1. Basics
Gotcha #1: Excessive Commenting
Gotcha #2: Magic Numbers
Gotcha #3: Global Variables
Gotcha #4: Failure to Distinguish Overloading from Default Initialization
Gotcha #5: Misunderstanding References
Gotcha #6: Misunderstanding Const
Gotcha #7: Ignorance of Base Language Subtleties
Gotcha #8: Failure to Distinguish Access and Visibility
Gotcha #9: Using Bad Language
Gotcha #10: Ignorance of Idiom
Gotcha #11: Unnecessary Cleverness
Gotcha #12: Adolescent Behavior
Chapter 2. Syntax
Gotcha #13: Array/Initializer Confusion
Gotcha #14: Evaluation Order Indecision
Gotcha #15: Precedence Problems
Gotcha #16: 'for' Statement Debacle
Gotcha #17: Maximal Munch Problems
Gotcha #18: Creative Declaration-Specifier Ordering
Gotcha #19: Function/Object Ambiguity
Gotcha #20: Migrating Type-Qualifiers
Gotcha #21: Self-Initialization
Gotcha #22: Static and Extern Types
Gotcha #23: Operator Function Lookup Anomaly
Gotcha #24: Operator '->' Subtleties
Chapter 3. The Preprocessor
Gotcha #25: '#define' Literals
Gotcha #26: '#define' Pseudofunctions
Gotcha #27: Overuse of '#if'
Gotcha #28: Side Effects in Assertions
Chapter 4. Conversions
Gotcha #29: Converting through 'void *'
Gotcha #30: Slicing
Gotcha #31: Misunderstanding Pointer-to-Const Conversion
Gotcha #32: Misunderstanding Pointer-to-Pointer-to-Const Conversion
Gotcha #33: Misunderstanding Pointer-to-Pointer-to-Base Conversion
Gotcha #34: Pointer-to-Multidimensional-Array Problems
Gotcha #35: Unchecked Downcasting
Gotcha #36: Misusing Conversion Operators
Gotcha #37: Unintended Constructor Conversion
Gotcha #38: Casting under Multiple Inheritance
Gotcha #39: Casting Incomplete Types
Gotcha #40: Old-Style Casts
Gotcha #41: Static Casts
Gotcha #42: Temporary Initialization of Formal Arguments
Gotcha #43: Temporary Lifetime
Gotcha #44: References and Temporaries
Gotcha #45: Ambiguity Failure of 'dynamic_cast'
Gotcha #46: Misunderstanding Contravariance
Chapter 5. Initialization
Gotcha #47: Assignment/Initialization Confusion
Gotcha #48: Improperly Scoped Variables
Gotcha #49: Failure to Appreciate C++'s Fixation on Copy Operations
Gotcha #50: Bitwise Copy of Class Objects
Gotcha #51: Confusing Initialization and Assignment in Constructors
Gotcha #52: Inconsistent Ordering of the Member Initialization List
Gotcha #53: Virtual Base Default Initialization
Gotcha #54: Copy Constructor Base Initialization
Gotcha #55: Runtime Static Initialization Order
Gotcha #56: Direct versus Copy Initialization
Gotcha #57: Direct Argument Initialization
Gotcha #58: Ignorance of the Return Value Optimizations
Gotcha #59: Initializing a Static Member in a Constructor
Chapter 6. Memory and Resource Management
Gotcha #60: Failure to Distinguish Scalar and Array Allocation
Gotcha #61: Checking for Allocation Failure
Gotcha #62: Replacing Global New and Delete
Gotcha #63: Confusing Scope and Activation of Member 'new' and 'delete'
Gotcha #64: Throwing String Literals
Gotcha #65: Improper Exception Mechanics
Gotcha #66: Abusing Local Addresses
Gotcha #67: Failure to Employ Resource Acquisition Is Initialization
Gotcha #68: Improper Use of 'auto_ptr'
Chapter 7. Polymorphism
Gotcha #69: Type Codes
Gotcha #70: Nonvirtual Base Class Destructor
Gotcha #71: Hiding Nonvirtual Functions
Gotcha #72: Making Template Methods Too Flexible
Gotcha #73: Overloading Virtual Functions
Gotcha #74: Virtual Functions with Default Argument Initializers
Gotcha #75: Calling Virtual Functions in Constructors and Destructors
Gotcha #76: Virtual Assignment
Gotcha #77: Failure to Distinguish among Overloading, Overriding, and Hiding
Gotcha #78: Failure to Grok Virtual Functions and Overriding
Gotcha #79: Dominance Issues
Chapter 8. Class Design
Gotcha #80: Get/Set Interfaces
Gotcha #81: Const and Reference Data Members
Gotcha #82: Not Understanding the Meaning of Const Member Functions
Gotcha #83: Failure to Distinguish Aggregation and Acquaintance
Gotcha #84: Improper Operator Overloading
Gotcha #85: Precedence and Overloading
Gotcha #86: Friend versus Member Operators
Gotcha #87: Problems with Increment and Decrement
Gotcha #88: Misunderstanding Templated Copy Operations
Chapter 9. Hierarchy Design
Gotcha #89: Arrays of Class Objects
Gotcha #90: Improper Container Substitutability
Gotcha #91: Failure to Understand Protected Access
Gotcha #92: Public Inheritance for Code Reuse
Gotcha #93: Concrete Public Base Classes
Gotcha #94: Failure to Employ Degenerate Hierarchies
Gotcha #95: Overuse of Inheritance
Gotcha #96: Type-Based Control Structures
Gotcha #97: Cosmic Hierarchies
Gotcha #98: Asking Personal Questions of an Object
Gotcha #99: Capability Queries
Bibliography

Gotcha #82: Not Understanding the Meaning of Const Member Functions

Syntax

One of the first things one notices about const member functions is the rather unnerving syntax used to specify them. That const stuck onto the end of the declaration just looks like a hack. It isn't. Like the rest of the declaration syntax C++ inherits from C, the syntax for declaring a const member function is both logically consistent and confusing:

class BoundedString { 
 public:
   explicit BoundedString( int len );
   // . . .
   size_t length() const;
   void set( char c );
   void wipe() const;
 private:
   char * const buf_;
   int len_;
   size_t maxLen_;
};

Let's look first at the declaration of the private data member buf_, which is declared to be a constant pointer to character (this is an illustrative example; see Gotcha #81). The pointer is constant, not the characters it points to, so the const type-qualifier follows the pointer modifier. If we had put const before the asterisk, it would refer to the char base type, and we'd have declared a non-const pointer to constant characters.

The same is true of the const member function length. If we had put the const before the name of the function, we would have declared a member function that takes no argument and returns a constant size_t. The appearance of const after the function modifier indicates that the function is const, not its return value.

Simple Semantics and Mechanics

What does it mean for a member function to be const? The usual answer to this question is simply that a const member function doesn't change its object. That's a simple statement, and it's simple for the compiler to implement.

Every non-static member function has an implicit argument that is a pointer to the object used to call the member function. Within the function, the this keyword gives the value of the pointer:

BoundedString bs( 12 ); 
cout << bs.length(); // "this" is &bs
BoundedString *bsp = &bs;
cout << bsp->length(); // "this" is bsp

For a non-const member function of a class X, the type of the this pointer is X * const; that is, it's a constant pointer to a non-constant X. The pointer itself may not be modified (and therefore this will always refer to the same X object), but the members of X may be modified. Within a non-const member function, any access to a non-static class member is accomplished through a pointer to non-const:

void BoundedString::set( char c ) { 
   for( int i = 0; i < maxLen_; ++i )
       buf_[i] = c;
   buf_[maxLen_] = '\0';
}

For a const member function of a class X, the type of the this pointer is const X * const; it's a constant pointer to a constant X. Neither the pointer nor the object it points to can be changed:

size_t BoundedString::length() const 
   { return strlen( buf_ ); }

Essentially, a const member function gives us a way to specify the constness of the implicit this argument of a member function. For example, consider the declaration of a non-member equality operator for BoundedString:

bool operator ==( const BoundedString &lhs, 
                 const BoundedString &rhs );

The function doesn't change its arguments—it only examines them—and therefore both the left and right arguments are declared to be reference to const. The same should be the case for an analogous member function:

class BoundedString { 
   // . . .
   bool operator <( const BoundedString &rhs );
   bool operator >=( const BoundedString &rhs ) const;
};

Remember that the left argument of an overloaded binary member operator function is passed implicitly to the function as the this pointer. The right argument is used to initialize the explicitly declared formal argument (named rhs in the two member operator functions above). The greater-than-or-equal-to operator is properly declared, and the function makes guarantees not to change either the left or right arguments. However, the less-than operator is improper, in that it guarantees the safety of the right argument without making any such promise for the left argument. This impropriety will probably show up when we try to implement >= in the most straightforward way:

bool BoundedString::operator >=( const BoundedString &rhs ) const 
   { return !(*this < rhs); }

We'll get a compile-time error in the call to operator <. When we pass the expression *this as the first argument to operator <, we're attempting to initialize the this pointer of a non-const member function with the address of a constant object.

The Meaning of a Const Member Function

We've described the mechanics of const member functions above, but the meaning of const member functions is, to a large extent, socially determined by the community of competent C++ programmers. Consider an implementation of the wipe member of BoundedString:

void BoundedString::wipe() const 
   { buf_[0] = '\0'; }

This is legal, but just because something is legal doesn't mean it's either morally permissible or expected. The wipe function doesn't change its object; that is, it doesn't modify any of BoundedString's data members. However, it does change data outside the object that affects the behavior of the object. The logical state of the BoundedString object will have changed after a call to wipe. The constness of the this pointer affects access only to the data members within the BoundedString object itself. Data outside the object are not included in this protection, but the data are nevertheless part of the logical state of the BoundedString object.

Most users of BoundedString would be unpleasantly surprised to find that the behavior of their object had been modified by a call to a const member function. Because wipe changes the logical state of its object, it should not be declared to be const. That's why our earlier definition of the set member was declared to be non-const, even though the compiler would have permitted it to be declared const.

Conversely, let's look at the implementation of the length member function. This is a function that clearly should be const, since determining the length of a BoundedString doesn't change its logical state. The most straightforward implementation would employ the standard library function strlen, as we did above. This is probably the best implementation, since it's simple, reasonably fast, and gives the correct result. However, suppose we observe that many strings never have their lengths taken, many others have their lengths taken repeatedly, and that strings tend to be long. In that case, a different implementation might be preferable:

size_t BoundedString::length() const { 
   if( len_ < 0 )
       len_ = strlen( buf_ );
   return len_;
}

In this case, we've decided to store the current string length within the BoundedString object and to perform a "lazy evaluation" of the string length. Therefore there is little runtime cost in the event that the string length is never taken and minimal for repeated calls to length. Unfortunately, the compiler will issue an error when we attempt to assign a value to len_. This is a const member function and is not allowed to change its object.

We could deal with this problem by making length non-const, but this defeats the logical intent of the function and wouldn't allow us to determine the length of a BoundedString declared to be const (whether it's actually const or not; see Gotchas #6 and #31). We'd be making length non-const due to an implementation issue, but, to the extent practical, implementation issues shouldn't affect the interface of an abstract data type.

A common and reprehensible practice in a situation like this is to "cast away const" in the const member function:

size_t BoundedString::length() const { 
   if( len_ < 0 )
       const_cast<int &>(len_) = strlen( buf_ );
   return len_;
}
// . . .
BoundedString a(12);
int alen = a.length(); // will work . . .
const BoundedString b(12);
int blen = b.length(); // undefined!

Any attempt to modify a constant object outside its constructors or destructor results in undefined behavior. Therefore, calling the length member function on b may work—or may fail mysteriously long after the code has been tested and delivered. That the cast is a newfangled const_cast doesn't help in the least.

The proper solution is to declare the len_ data member to be mutable. The mutable storage-class-specifier may be applied to a non-static, non-const, nonreference data member to indicate that it may be safely modified by const (as well as non-const) member functions.

class BoundedString { 
   // . . .
 private:
   char * const buf_;
   mutable int len_;
   size_t maxLen_;
};

For the community of C++ programmers, a const member function implements "logical" constness. That is, the observable state of an object is not changed by a call to a const member function, even though its physical state may be.