Item03_const

只要明确值不被改变就应该声明为 const ,以此获得编译器的襄助,保证规则不被违反。

顶层 const 与 底层 const

const pointer 成为顶层 const, pointer 指向的数据为 const 称为底层 const.

1
2
3
4
5
char greeting[] = "hello world"; 	// greeting 类型为 char [12]
const char *p = greeting; // 添加底层const const data , non-const pointer
char * const p = greeting; // 顶层const non-const data , const pointer
const char * const p =greeting // const data , const pointer

底层 const 的以下两种写法等价

1
2
const char *p;		
char const *p;

STL 中的 iterator 类似于 (T*) 指针。声明迭代器为 const 类似于 T * const ,顶层const。而如果需要迭代器所指向的数据不被修改,需要 const_iterator

1
2
3
4
5
6
7
std::vector<int> v{1,2,3};
const std::vector<int>::iterator it = v.begin(); //顶层const
*it = 1; //正确,可被修改
++it; //错误,不能修改迭代器本身
std::vector<int>::const_iterator itt = v.begin(); //底层const
*itt = 1; //错误,被指向的数据不可被修改
++itt; //正确,可以递增迭代器本身

函数返回值

将函数范围值声明为 const 可以减少不必要的麻烦,比如

1
2
3
4
class Rational {
};
...
const Rational *operator(const Rational &lhs, const Rational &rhs) {...}

如果去掉返回值的 const ,你的用户可能做出如下行为

1
(3*5) = 3

你可能觉得根本不会有人做出怎么奇怪的操作,但是下面这个错误就比较隐蔽, 用户在条件语句中将 == 写成了 =,发生隐式 bool 类型的转化:

1
if( (3*5) = 3)

尽量将使用 reference-to-const 作为参数不仅避免拷贝,还能绑定到右值,比如:

1
2
3
4
5
void foo (std::string & s)
...
std::string str;
foo(str) //正确,绑定到左值
foo("hello") //错误,不能绑定右值 "hello"

尽管 const 只有六个字符,但是却能避免很多不必要的麻烦。

const 成员函数

目的

  1. 使得 class 接口容易被理解,明白哪些函数改动内容,而哪些不行
  2. 使得 ‘操作 const 对象’ 成为可能。因为很多情况需要使用 pass by reference-to-const ,这项技术的前提就是需要 const 成员函数。
1
2
3
4
5
6
7
8
9
10
11
12
class TextBlock
{
public:
const char &operator[](std::size_t pos) const {
return text[pos];
}
char &operator[](std::size_t pos) {
return text[pos];
}
private:
std::string text;
};

下面是调用 operator [] 的例子

1
2
3
void print(const TextBlock & str) { //str 是 const 对象
std::cout << str[0]; //调用 const 成员函数
}

如果 operator [] 的返回值类型是 char reference ,如果是 char 的话,下面句子无法编译:

1
str[0] = 'x'

bitwise const and logical const

  • bitwise const: class 内部的成员变量不能修改 ,编译器检测的方式
  • logical const: 设计者期望的常量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CTextBlock
{
public:
CTextBlock(char *str):p(str){
}
char & operator[](std::size_t pos) const {
return p[pos];
}
private:
char *p;
};
...
char str[] = "hello";
const CTextBlock ccb(str);
char *p = &ccb[0];
*p = 'J'; //还是被修改了

这里我们想要的 const 是 p 的内部值不被改变,但是 operator[] 返回值没加 const 时也能编译成功,即逃过了编译器 bitwise const 检查。所以尽可能使用 STL 中的 string 而非手动管理内存。

下面在介绍一个 logical const 被 编译器 bitwise const 所过度限制的例子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
std::size_t textlength;
bool lengthIsvalid;
};

std::size_t CTextBlock::length() const {
if(!lengthIsvalid) {
textlength = std::strlen(pText);
lengthIsvalid = true;
}
return textlength;
}

显然 length() 不是 bitwise const , 因为 textlength 发生了改变。但是对于我们而言,textlengthlengthIsvalid 是可以接受的。 我们可以使用 mutable 关键字释放掉 non-static 成员变量 bitwise const 的约束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CTextBlock
{
public:
std::size_t length() const;
private:
char *pText;
mutable std::size_t textlength; //现在可在 const 成员函数中被修改
mutable bool lengthIsvalid;
};

std::size_t CTextBlock::length() const {
if(!lengthIsvalid) {
textlength = std::strlen(pText);
lengthIsvalid = true;
}
return textlength;
}

在 const 成员函数和 non-const 成员函数中避免重复

在对某个函数实现 const 与 non-const 重载时,会产生大量的代码重复,比如在上面的例子中放入更多复杂的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TextBlock
{
public:
const char &operator[](std::size_t pos) const {
//do somethings
return text[pos];
}
char &operator[](std::size_t pos) {
// do somethings
return text[pos];
}
private:
std::string text;
};

我们应该考虑令其中的一个调用另一个,这是一种很重要的思想,在重载 +=+ 是也利用之。利用 const 成员函数实现 non-const

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class TextBlock
{
public:
const char &operator[](std::size_t pos) const {
//do somethings
return text[pos];
}
char &operator[](std::size_t pos) {
return const_cast<char &>( //移除 op[] 的 const
static_cast<const TextBlock&>(*this)[pos]); //为当前对象添加 const
}
private:
std::string text;
};

这里用到两次转型 分别是为 *this 添加 const , 为 op[ ] 的返回值移除 const. 注意 const_caststatic_const 的区别。只有 const_cast 能移除 const。

但是反向做法:利用 non-const 成员函数实现 const 是不正确的。这是一个危险的行为,首先 non-const 函数并未声明不对对象进行修改,而 const 成员函数依赖与一个改变对象的函数显然是不合理的。而且你不得不使用 const_cast 去除 const,最终实现的 const 成员函数不能保证真正的 const. const 成员函数声明也就失效了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class TextBlock
{
public:
const char &operator[](std::size_t pos) const {
//do somethings
return static_cast<const char &>( //添加 op[] 的 const
const_cast<TextBlock&>(*this)[pos]); //为当前对象移除 const
}
char &operator[](std::size_t pos) {
// do somethings
text[pos] = 'a';
return text[pos];
}
private:
std::string text;
};

总结

  • 将某些东西声明为 const 可帮助编译器侦测错误用法。const 可作用于对象,函数参数,函数返回值类型,成员函数本体。
  • 编译器实施 bitwise constness ,但编写程序时应使用 conceptual constness.
  • non-const 版本调用 const 版本避免代码重复

参考

Effective C++ 中文版(第三版)