[ Team LiB ] |
![]() ![]() |
Gotcha #82: Not Understanding the Meaning of Const Member FunctionsSyntaxOne 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 MechanicsWhat 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 FunctionWe'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. ![]() |
[ Team LiB ] |
![]() ![]() |