偶然有这种情况,一个成员函数在逻辑上是const,但它却仍需要改变某个成员的值。对于用户而言,这个函数看似没有改变其对象的状态,然而,它却可能更新了某些用户不能直接访问的细节。这通常被称为逻辑的常量性。例如,Date类可能有一个函数,它应返回一个用户可以用于打印的字符串表示。构造出这种表示可能是一个相对费时的操作,因此,保留一个副本,在重复需要的时候直接返回这个副本,这一做法也就有意义了,除非这个Date值被改变。
class Date { public: string stringRep() const; //... private: bool cacheValid; string cache; void computeCacheValue(); //... };
从用户的角度看,stringRep并没有改变它的Date的状态,所以它应该是个const成员函数。而在另一方面,在使用之前必须填充缓存,要做到这一点只能通过蛮力:
string Date::stringRep() const { if (!cacheValid) { Date *th = const_cast<Date *>(this); th->computeCacheValue(); th->cacheValid = true; } return cache; }
也就是说,这里用const_cast运算符从this获得一个Date *指针。这当然一点也不优美,而且它也无法保证总能工作,例如,当被应用的对象原本就是一个const的时候。例如:
Date d1; const Date d2; string s1 = d1.stringRep(); string s2 = d2.stringRep(); //undefined
对于d1的情况,stringRep()简单地将其强制转回原来的类型,所以这个调用能够完成。然而,d2原本定义为const,具体实现很有可能为保护它的值不被破环而使用某种特殊形式存储。因此,无法保证d2.stringRep()能在所有实现上都给出同样的可预见的结果。
显式类型转换“强制去掉const”,以及由它引起的依赖于实现的行为还是可以避免的,只要将缓存管理所涉及的数据声明为mutable:
class Date { public: string stringRep() const; //... private: mutable bool cacheValid; mutable string cache; void computeCacheValue() const; //... };
存储描述符mutable特别说明这个成员需要以一种能允许更新的方式存储——即使它是某个const对象的成员。换言之,mutable意味着“不可能是const”。这种机制可用于简化stringRep()的定义。
string Date::stringRep() const { if (!cacheValid) { computeCacheValue(); cacheValid = true; } return cache; }
这就使stringRep()的合理使用都能合法化了。例如,
Date d1; const Date d2; string s1 = d1.stringRep(); string s2 = d2.stringRep(); //ok
如果在某个表示中(只有)一部分允许改变,将这些成员声明为mutable是最合适的。如果一个对象在逻辑上保持为const的同时,其中的大部分需要修改,那么最好将这些需要修改的数据放入另一个独立的对象里,并间接地访问它。假设采用这种技术,使用缓存的字符串将变成
struct Cache { bool valid; string rep; }; calss Date { public: //... string stringRep() const; //字符串表示 private: Cache *c; //在构造函数里初始化 void computeCacheValid() const; //填充引用的缓存 //... }; string Date::stringRep() const { if (!c->valid) { computeCacheValid(); c->valid = true; } return c->rep; }
也可以将
Cache *c;
改为
Cache &c;
但是不能改为
Cache c;
因为如果是指针或引用,Date对象中包含的属性是类似Cache对象的地址,虽然改变了Cache对象的状态,但是其地址并没有改变,所以对于Date对象而言,其状态并没有改变。但是如果Date对象直接包含一个Cache对象的话,Cache对象状态的改变间接改变了Date对象的状态。