Fork me on GitHub

读书笔记 effective c++ Item 3 在任何可能的时候使用 const

Const可以修饰什么?

 

Const 关键字是万能的,在类外部,你可以用它修饰全局的或者命名空间范围内的常量,也可以用它来修饰文件,函数和块作用域的静态常量。在类内部,你可以使用它来声明静态或者非静态的数据成员。对于指针来说,你可以指定指针本身是不是const,指针指向的数据是不是const,两者可以同时为const或者两者同时为非const.

  

1 Char greeting[]=”Hello”;
2 Char *p = greeting;//non-const pointer non-const data 
3 Const char *p = greeting; //non-const pointer const data
4 Char *const p = greeting;//const pointer non-const data
5 Const char *const p = greeting;//const pointer const data.

 

在指针中const的位置说明

 

这项语法并不像看上去那么反复无常,如果关键字const出现在星号的左侧,那么指针指向的内容为const,如果关键字const出现在星号的右侧,那么指针本身是const;如果星号出现在两侧,那么两者都为const.

当指针指向的内容为const时,一些程序员将const放在类型之前,一些程序员将const放在类型之后,星号之前,这两者在意义上是等同的,下面的两个函数有相同的参数类型:

void f1(const Widget *pw);
void f2(Widget const *pw);

因为两种形式在代码中都是存在的,因此应该能够识别两者。 

Const在迭代器中应用 

STL迭代器是模仿的指针,因此迭代器的行为非常像一个指针,声明一个迭代器的const就像声明一个指针的const: 迭代器不允许指向不同的东西,但迭代器指向的东西是可以改变的。如果你想让迭代器指向的东西不能被修改,这时你就需要一个const_iterator.

std::vector<int> vec;
…
const std::vector<int>::iterator iter= vec.begin();//iter acts like a T*const
*iter = 10;//OK
++iter;//error
std::vector<int>::const_iterator citer = vec.begin();//citer acts like a T const * *citer = 10;//error ++citer;//OK

  

Const修饰函数返回值和函数参数

 

Const 的一些强大的用法来源于在函数声明上的应用。在一个函数声明中,const可以修饰函数返回值,函数参数,对于成员函数来说,可以修饰整个函数。

一个函数返回一个const值在一般情况下是不合适的。但有时在保证安全和效率的情况下可以减少客户端的出错率。举个例子,为有理数声明一个operator*函数。

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

为什么operator*的结果是一个const对象?因为如果不是const,客户端可以提交以下的暴行:

1 Rational a,b,c;
2 
3 (a*b)=c;//invoke operator= on the result of a*b

我不知道为什么程序员会对两个数的乘积进行赋值,但我确实知道程序员虽然不想这么做,但确实这么做了。所有都是因为一个简单的输入错误(一个可以隐式转换成bool的类型):

If(a*b=c)…

如果a和b是内建类型,那么上面的代码是不合法的,一个好的用户自定义类型的特点是它们避免同内建类型的无端的不兼容(上面的代码如果内建类型不合法,那么用户自定义类型也应该不合法)。因此对两个数的乘积进行赋值也就没有理由这么做了。将operator*的返回值声明成const可以阻止这种赋值操作。

对于const参数来说没有什么特别新的,它们就像local的const对象,你应该在任何可以使用它们的时候使用它。除非你像更改一个参数或者本地对象,否则确保它被声明成const,敲打六个字符的努力会可以使你免于类似上面的错误的打搅。

Const 成员函数介绍

 

将const作用于成员函数上面的意图是能够确认哪些成员函数可以通过const对象被调用。这类成员函数很重要,原因有两条:一,它们使得类的接口更容易被理解,知道哪些函数可以改变对象哪些不可以,这一点很重要。二,它们使得和const对象一起工作成为可能,这是编写高效代码的很重要的方面,因为Item20解释道,提高c++程序性能的基本方法是按const引用传递对象。这项是可行的前提是,const成员函数能够处理const修饰的对象。

许多人忽略了一个事实,不同常量性的两个成员函数是可以重载的,这时c++的重要特性。考虑一个表现文本块的类:

 1 class TextBlock
 2 
 3 {
 4 
 5   public:
 6 
 7      8 
 9     const char&operator[](std::size_t positioin) const
10 
11     {return text[position];}
12 
13     char&operator[](std::size_t position)
14 
15     {return text[position];}
16 
17   private:
18 
19      std::string text;
20  };

TextBlock 的operator[]函数可以像下面这样使用:

1 TextBlock tb(“Hello”);
2 
3 Std::cout<tb[0];//call non-const operator[]
4 
5  
6 
7 Const TextBlock ctb(“World”);
8 
9 Std::cout<<ctb[0];//call const TextBlock::operator[]

顺便说一下,将指向const对象的指针或者指向const对象的引用作为参数经常出现在真实世界的程序中。上面的关于ctb的例子过于造作。下面的例子更符合实际:

void Print(const TextBlock &ctb)

{
    std::cout<<ctb[0];//call const TextBlock::operator[]
}

通过重载operator[],给不同的函数版本不同的返回值类型,可以对 const和非const TextBlocks进行不同处理:

1 std::cout << tb[0]; // fine — reading a non-const TextBlock
2 
3 tb[0] = ’x’; // fine — writing a non-const TextBlock
4 
5 std::cout << ctb[0]; // fine — reading a const TextBlock
6 
7 ctb[0] = ’x’; // error! — writing a const TextBlock

 

注意这里错误的出现是由于调用operator[]的返回值引起的,调用operator[]本身没有问题。问题出现在尝试给一个const char&类型的变量进行赋值操作, const char&是const 版本的operator[]的返回值类型。

同时需要注意non-const operator[]的返回值类型是指向char的引用,如果operator[]返回一个char,那么下面的句子将不能通过编译:

1 tb[0] = ‘x’;

因为尝试修改内建类型的函数返回值是不合法的。即使是合法的,c++中的按值返回(Item20)意味着我们修改的是tb.text[0]的拷贝,而不是tb.text[0]本身,这不是你需要的行为。

Const成员函数的两个派别

 

将成员函数声明成const意味这什么,对于这个问题有两种流行的见解:bitwise
constness
(also known as physical constness) and logical constness

Bitwise  const阵营相信当且仅当成员函数不修改对象的任何数据成员(不包括statics数据成员)时它才是const成员函数,举个例子:成员函数没有修改对象内部的任意bits.bitwise const的一个好处是能够比较容易的识别出反例:编译器只要寻找对数据成员的赋值就可以了。事实上,bitwise const是常量性的c++定义,const成员函数不允许修改对象的任何非静态数据成员。

不幸的是,许多成员函数没有表现的特别常量性,而不能通过bitwise-const测试。特别的,一个成员函数修改指针指向的内容没有表现出常量性。但是如果指针是在对象内部,这个函数是bitwise const的,编译器不会发出抱怨。这会导致一个违反直觉的行为。举个例子,我们有个像testBlock的类,用char*而不是string来存储数据,因为它需要同一个不是识别string的C-API进行通讯

 1 class CTextBlock {
 2 
 3 public:
 4 
 5 ...
 6 
 7 char& operator[](std::size_t position) const // inappropriate (but bitwise
 8 
 9 { return pText[position]; }                  // const) declaration of operator[]
10 
11 private:
12 
13 char *pText;
14 
15 };

 

这个类(不恰当的)将operator[]声明为一个const成员函数,而函数返回的是指向对象内部数据的引用(更深层次的讨论见Item28)。把它抛到一边,注意到operator[]的实现没有以任何方式修改pText。因此,编译器很高兴的为operator[]生成了代码,它毕竟是bitwise const的,这时编译器检查的所有东西,但是看一下允许发生什么: 

1 const CTextBlock cctb("Hello"); // declare constant object
2 
3 char *pc = &cctb[0];  // call the const operator[] to get a pointer to cctb’s data
4 
5 *pc = ’J’;                                       // cctb now has the value “Jello”

 

当你用特定值创建一个const对象的时候,这里出现了错误,你只是调用了对象的const成员函数,但是值仍然被修改了!

因此产生了logical constness的概念,这种观点的拥护者辩论到,一个const成员函数可以修改对象内部的一些bits,但是只有在客户端不能够内侦测到的情况下才行。

 

logical constness的两个问题 

  • 问题一 
 1 class CTextBlock {
 2 
 3 public:
 4 
 5 ...
 6 
 7 std::size_t length() const;
 8 
 9 private:
10 
11 char *pText;
12 
13 std::size_t textLength;     // last calculated length of textblock
14 
15 bool lengthIsValid;          // whether length is currently valid
16 
17 };
18 
19 std::size_t CTextBlock::length() const
20 
21 {
22 
23 if (!lengthIsValid) {
24 
25 textLength = std::strlen(pText); // error! can’t assign to textLength
26 
27 lengthIsValid = true;                    // and lengthIsValid in a const
28 
29 }                                               // member function
30 
31 return textLength;
32 
33 }

 

Length()的这种实现当然不是bitwise const的,textLength和lengthIsValid都有可能被修改。但是看上去这个函数对const CTextBlock对象来说应该是有效的。编译器不同意。它们坚持bitwise constness。该怎么做?

解决方法比较简单:利用c++常量相关的扭动空间,也就是mutable,mutable将数据成员从bitwise constness的约束中释放出来:

 1 class CTextBlock {
 2 
 3 public:
 4 
 5 ...
 6 
 7 std::size_t length() const;
 8 
 9 private:
10 
11 char *pText;
12 
13 mutable std::size_t textLength;               // these data members may
14 
15 mutable bool lengthIsValid;                    // always be modified, even in const member functions
16 
17 std::size_t CTextBlock::length() const
18 
19 {
20 
21 if (!lengthIsValid) {
22 
23 textLength = std::strlen(pText);          // now fine
24 
25 lengthIsValid = true;                            // also fine
26 
27 }
28 
29 return textLength;
30 
31 }

 

  • 问题二

Mutable对于bitwise-constness不是很介意的问题是一个好的解决方法,但是它不能够解决所有的const相关的难题。例如:假设TextBlock中的operator[]不仅返回一个指向合适字符的引用,它同时执行边界检查,为访问信息打印日志,可能甚至会检查数据完整性。将所有这些同时放在const和non-const operator[](Item30)函数中,产生了下面这种怪胎:

 

 1 class TextBlock {
 2 
 3 public:
 4 
 5 ...
 6 
 7 const char& operator[](std::size_t position) const
 8 
 9 {
10 
11 ... // do bounds checking
12 
13 ... // log access data
14 
15 ... // verify data integrity
16 
17 return text[position];
18 
19 }
20 
21 char& operator[](std::size_t position)
22 
23 {
24 
25 ... // do bounds checking
26 
27 ... // log access data
28 
29 ... // verify data integrity
30 
31 return text[position];
32 
33 }
34 
35 private:
36 
37 std::string text;
38 
39 };

 

你能说出代码重复,编译时间变长,不易维护,代码膨胀等令人头痛的问题么?当然,可以把边界检查等所有代码移到一个单独的成员函数中(当然是private的),两个版本的opeator[]都会调用这个函数,但是仍然会有重复调用函数,仍然会有重复的返回语句。

 

你真正需要的是之实现operator[]一次,而使用两次。也就是你需要用一个版本的operator[]去调用另一个版本的operator[]。这把我们带到了如何将constness去掉(casting)的问题。

 

作为一个通用的规则,casting是一个坏的方法,我用了一整个条款(Item27)来告诉你不要使用casting,但是代码重复也是不令人愉快的。operator[]的const版本做了non-const版本要做的所有事情,唯一的区别是有一个const返回类型。去掉返回值的常量性是安全的,任何人调用non-const operator[]必须在第一个位置有一个non-const的对象。否则它们不能够被叫做non-const函数。所以non-const operator[]调用const版本是防止代码重复的安全的方法,即使需要cast.下面是代码,你在读完后面的解释后可能会更加清楚。 

class TextBlock {

public:

...

const char& operator[](std::size_t position) const // same as before

{

...

...

...

return text[position];

}

char& operator[](std::size_t position)                          // now just calls const op[]

{

return

const_cast<char&>(                                                  // cast away const on op[]’s return type;

static_cast<const TextBlock&>(*this) [position]//add const to *this’s type call const version of op[]

);

}

...

};

 

你所看到的是,代码使用了两个cast,我们想让operator[]的non-const版本调用const版本,但是如果在non-operator[]内部,我们只是调用operator[],我们会递归的调用自己。为了防止无限的递归调用,必须指定我们调用的是const operator[],但是没有直接的方法做到这一点。因此我们需要将*this从原生类型TextBlock& 转换(cast)成const TextBlock&。是的,我们使用cast来添加const.因此我们会使用两个cast:一个为*this添加const,另一个将const从operator[]的返回值中移除。

 

添加const的cast只是强制进行安全转型(从non-const转换成const),所以我们使用static_cast.将常量性移除只有通过const_cast才能完成,我们没有别的选择。(从技术上来说,一个C风格的cast也能达到目的,但是,正如Item27所描述的,这样的cast不是正确的选择。如果你不熟悉static_cast或者const_cast,参考Item27).

 

在这个例子中,我们调用的是一个operator,所以语法看起来有些奇怪。这个结果不能赢得选美大赛,但是依赖const版本的函数来实现non-const版本的函数,达到了避免代码重复的效果。为了达到目标而写出如此难看的语法,是你能够决定的,但是依靠const函数来实现non-const成员函数的技术是绝对值得了解的。

 

更值得了解的是,反向调用-也就是通过const来调用non-const版本的函数来避免代码重复-不是你应该做的。需要记住,const成员函数承诺绝不修改对象的逻辑状态,但是non-const函数并没有这样的承诺。如果你从一个const成员函数中调用一个non-const成员函数,就会出现你承诺不应该被修改的东西最终被修改的风险,这也是为什么用const成员函数调用non-const成员函数是错误的:对象有可能被改变。事实上,为了是代码通过编译,你需要使用const_cast将*this的常量性去除,这是会出现麻烦的迹象。相反的调用顺序是安全的,一个non-const成员函数可以对一个对象做任何它想做的,所以调用const成员函数是没有危险。这也是为什么static_cast可以应用在*this上的原因:没有const相关的危险。

 

posted @ 2017-02-09 23:01  HarlanC  阅读(794)  评论(0编辑  收藏  举报