条款10 常量成员函数的含义
从技术上来说,常量成员函数很简单。从社会意义来说,它们却可能很复杂。
在类X的非常量成员函数中,this指针的类型为X *const。也就是说,它是指向非常量X的常量指针(参见“常量指针与指向常量的指针”[条款7])。由于this指向的对象不是常量,因此它可以被修改。而在类X的常量成员函数中,this的类型为const X* const。也就是说,是指向常量X的常量指针。由于指向的对象是常量,因此它不能被修改。这就是常量成员函数和非常量成员函数之间的区别。
这也是为什么可以使用常量成员函数来改变对象的逻辑状态原因,虽然对象的物理状态没有发生改变。考虑如下类X的实现,这个实现很普通,X使用一个指向已分配的缓冲区的指针,来保存它的一些状态:
- 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 // 不道德!
- { buffer_[index] = value; }
- int getValue() const {
- if( !isComputed_ ) {
- computedValue_ = expensiveOperation(); // 错误!
- isComputed_ = true; // 错误!
- }
- return computedValue_;
- }
- private:
- static int expensiveOperation();
- int *buffer_;
- bool isComputed_;
- int computedValue_;
- };
setBuffer成员函数必须是非常量的,因为它要修改其所属的X对象的一个数据成员。然而,modifyBuffer可以被合法地标为常量,因为它没有修改X对象,它只是修改X的buffer_成员所指向的一些数据。
这种做法是合法的,但很不道德。就像那些口口声声说尊重法律条文实际上却在违背其本意的奸诈律师一样,一个编写可以改变对象逻辑状态的常量成员函数的C++(www.cppentry.com)程序员,即使编译器未宣判他有罪,他的同事也会判他有罪,因为这种做法很不厚道!
话又说回来,有时一个真的应该被声明为常量的成员函数必须要修改其对象。这常见于利用“缓式求值”(lazy eva luation)机制来计算一个值时。换句话说,只有当第一次提出请求,才计算值,目的在于在该请求根本没有发出的其余情形下,让程序运行更快。函数X::getValue试图对一个代价高昂的计算执行“缓式评估”,但是,由于它被声明为常量成员函数,因此不允许它设置对象的isComputed_和computedValue_数据成员的值。在这种情况下会有一个进行转型犯错的诱惑,为的是能够让事情变得更好,即将该成员函数声明为常量:
- int getValue() const {
- if( !isComputed_ ) {
- X *const aThis = const_cast<X *const>(this); // 糟糕的念头!
- aThis->computedValue_ = expensiveOperation();
- aThis->isComputed_ = true;
- }
- return computedValue_;
- }
千万抵制住这个诱惑!处理这种情形的正确方式是将有关数据成员声明为mutable:
- class X {
- public:
- //...
- int getValue() const {
- if( !isComputed_ ) {
- computedValue_ = expensiveOperation(); // 很好
- isComputed_ = true; // 也很好
- }
- return computedValue_;
- }
- private:
- //...
- mutable bool isComputed_; // 现在可以修改了
- mutable int computedValue_; // 现在可以修改了
- };
类的非静态数据成员可以声明为mutable,这将允许它们的值可以被该类的常量成员函数(当然也包括非常量成员函数)修改,从而允许一个“逻辑上为常量”的成员函数被声明为常量,虽然其实现需要修改该对象。
对成员函数的this指针类型加上常量修饰,就可以解释函数重载解析是如何区分一个成员函数的常量和非常量版本的。下面是一个常见的重载索引操作符的例子:
- class X {
- public:
- //...
- int &operator [](int index);
- const int &operator [](int index) const;
- //...
- };
我们可以回想起二元重载成员操作符的左实参是作为this指针传入的。因此,当对一个X对象进行索引操作时,X对象的地址被作为this指针传入:
- int i = 12;
- X a;
- a[7] = i; // this是X *const,因为a是非常量
- const X b;
- i = b[i]; // this是const X *const,因为b是常量
重载解析会将常量对象的地址和指向常量的this指针相匹配。作为另一个例子,考虑如下具有两个常量参数的非成员二元操作符:
- X operator +( const X &, const X & );
如果决定声明一个此重载操作符的对应物,应该将其声明为常量成员函数,目的在于保持左实参的常量性质:
- class X {
- public:
- //...
- X operator +( const X &rightArg ); // 左边的参数是非常量!
- X operator +( const X &rightArg ) const; // 左边的参数是常量
- //...
- };
就像社会生活中的许多领域一样,在C++(www.cppentry.com)中正确地使用常量编程(www.cppentry.com)在技术上很简单,但在道德上(精神上)具有一定的挑战性。