April 21, 2009, 8:24 a.m.
posted by nogood
Item 10. Meaning of a Const Member FunctionTechnically, const member functions are trivial. Socially, they can be complex. The type of the this pointer in a non-const member function of a class X is X * const. That is, it's a constant pointer to a non-constant X (see Const Pointers and Pointers to Const [7, 21]). Because the object to which this refers is not const, it can be modified. The type of this in a const member function of a class X is const X * const. That is, it's a constant pointer to a constant X. Because the object to which this refers is const, it cannot be modified. That's the difference between const and non-const member functions. This is why it's possible to change the logical state of an object with a const member function even if the physical state of the object does not change. Consider the following somewhat uninspired implementation of a class X that uses a pointer to an allocated buffer to hold some portion of its state:
class X {
public:
X() : buffer_(0), isComputed_(false) {}
//...
void setBuffer() {
int *tmp = new int[MAX];
delete [] buffer_;
buffer_ = tmp;
}
void modifyBuffer( int index, int value ) const // immoral!
{ buffer_[index] = value; }
int getValue() const {
if( !isComputed_ ) {
computedValue_ = expensiveOperation(); // error!
isComputed_ = true; // error!
}
return computedValue_;
}
private:
static int expensiveOperation();
int *buffer_;
bool isComputed_;
int computedValue_;
};
The setBuffer member function must be non-const because it's modifying a data member of its X object. However, modifyBuffer can legally be const because it's not changing the X object; it's changing only some data to which the buffer_ member of X refers. That's legal, but it's not moral. Like a shyster lawyer who follows the letter of the law while violating its intent, a C++ programmer who writes a const member function that changes the logical state of its object will be judged guilty by his or her peers, if not by the compiler. It's just wrong. Conversely, sometimes a member function that really should be declared to be const must modify its object. This is a common situation where a value is computed by "lazy evaluation." That is, the value is not computed until the first request for it in order to speed things up in the event that the request isn't made at all. The function X::getValue is attempting to perform a lazy evaluation of an expensive computation, but, because it is declared to be a const member function, it is not allowed to set the values of the isComputed_ and computedValue_ data members of its X object. There is a temptation in cases like this to commit the crime of casting in order to promote the greater good of being able to declare the member function to be const:
int getValue() const {
if( !isComputed_ ) {
X *const aThis = const_cast<X *const>(this); // bad idea!
aThis->computedValue_ = expensiveOperation();
aThis->isComputed_ = true;
}
return computedValue_;
}
Resist the temptation. The proper way to handle this situation is to declare the relevant data members to be mutable:
class X {
public:
//...
int getValue() const {
if( !isComputed_ ) {
computedValue_ = expensiveOperation(); // fine...
isComputed_ = true; // also fine...
}
return computedValue_;
}
private:
//...
mutable bool isComputed_; // can now be modified
mutable int computedValue_; // can now be modified
};
Non-static data members of a class may be declared to be mutable, which will allow their values to be modified by const member functions of the class (as well as by non-const member functions). This in turn allows a "logically const" member function to be declared to be const even though its implementation requires modification of its object. The effect of const on the type of a member function's this pointer also explains how function overload resolution can distinguish between const and non-const versions of a member function. Consider the following omnipresent example of an overloaded index operator:
class X {
public:
//...
int &operator [](int index);
const int &operator [](int index) const;
//...
};
Recall that the left argument of a binary overloaded member operator is passed as the this pointer. Therefore, in indexing an X object, the address of the X object is passed as the this pointer: int i = 12; X a; a[7] = i; // this is X *const because a is non-const const X b; i = b[i]; // this is const X *const because b is const Overload resolution will match the address of a const object with a this pointer that points to a const. As another example, consider the following non-member binary operator with two const arguments: X operator +( const X &, const X & ); If we decide to declare a member analog to this overloaded operator, we should declare it to be a const member function in order to preserve the constness of the left argument:
class X {
public:
//...
X operator +( const X &rightArg ); // left arg is non-const!
X operator +( const X &rightArg ) const; // left arg is const
//...
};
Like many areas of social life, proper programming with const in C++ is technically simple but morally challenging. |
- Comment