我们可以定义具名的常量对象或者变量对象。换句话说,一个名字指向既可以是一个保存不可变值的对象,也可以是一个保存可变值的对象。
为了使得不可变性不局限于内置类型的简单常量的定义,我们必须能定义可操作用户自定义const对象的函数。对独立函数而言,这意味着函数可接受const T&参数。对类而言,这意味着我们斌能定义操作const对象的成员函数。
常量成员函数
class Date{
int y, m, d;
public:
int day() const {return d;}
int month() const {return m;}
int year() const;
void add_year(int n) { y += n ;};
};
函数声明中(空)参数列表后的const指出这些函数不会修改Date的状态。
自然的,编译期会捕获试图违法此承诺的代表,比如:
int Date::year() const {
return ++y; // 错误,试图在const函数中改变成员值
}
当const成员函数在类外时,必须使用const后缀:
int Date::year() { // 错误,在成员函数类型中漏掉了const
return y;
}
const和非const对象都可以调用const成员函数,而非const成员函数只能被非const对象调用:
void f(Date &d, const Date &cd){
d.year();
d.add_year(1);
cd.year();
cd.add_year(1); // error, 不能改变const Date的值
}
物理常量性和逻辑常量性
有时,一个成员函数逻辑上是const,但是它仍然需要改变成员的值。即对一个用户而言,函数看起来不会改变起对象的状态,但它更新了用户不能直接观察的某些细节。这通常叫做逻辑常量性。比如,类Date可能有一个返回字符串表示的函数,构造这个字符串表示非常耗时,因此,保存一个拷贝,在反复要求获取字符串表示时可以简单的返回此包括(除非Date的值已经被改变),这就很有意义了。更复杂的数据结构常使用这种缓存值的技术,现在我们来讨论如何对Date使用这个技术:
class Date{
public:
string string_rep() const;
private:
bool cache_valid;
string cache;
void compute_cache_value(); //填入缓存
};
从用户的角度来看,string_rep并没有改变起Date的状态,因此它显然应该是一个const成员函数。但是另一方面,有时必须改变成员cache和cache_valid,这种设计才能生效。
此问题也可以通过使用类型转换来解决,比如const_cast。但是,也存在非常优雅的,不破坏类型规则的方法。
mutable
我们可以将一个类成员定义为mutable,表示即使是在const对象中,也可以修改此成员:
class Date{
public:
string string_rep() const; //字符串表示
private:
mutable bool cache_valid;
mutable string cache;
void compute_cache_value() const; //填入(可变的)缓存
};
string::Date::string_rep() const{
if(!cache_valid){
compute_cache_value();
cache_valid = true;
}
return cache;
};
void f(Daye d, const Date cd){
d.string_rep(); //ok
cd.string_rep(); //ok
}
间接访问实现可变性
对于小对象的表示形式只有一小部分允许改变的形式,将成员声明为mutable是最合适的。但在更复杂的情况下,通常更好的方式是将需要改变的数据放在一个独立对象中,间接的访问它们:
struct cache{
bool cache_valid;
string rep;
};
class Date{
public:
string string_rep() const; //字符串表示
private:
cache * c; //在构造函数中初始化
void compute_cache_value() const; //填入cache指向的内存
};
string::Date::string_rep() const{
if(!c->valid){
compute_cache_value();
c->valid = true;
}
return c->rep;
}
这种支持缓存的编程技术可以推广到各种形式的懒惰求值。
注意:const不能(传递)应用到通过指针或者引用访问的对象。程序的读者可能会认为这种对象是”某种子对象“,但是编译期不能将这种指针或引用与其他指针或引用区分开来。即一个成员指针没有任何与其他指针不同的特殊语义。