Effective C++ 03 尽可能使用const 笔记

     const允许你告诉编译器和其他程序员某值应该保持不变,编译器会强制实施这项约束。const可以用在classes外部修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象;也可以修饰classes内部的static和non-static成员变量;也可以指出指针自身、指针所指物、或两者都(或都不)是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写在类型之前与之后意义相同。

     STL迭代器以指针为根据构造出来,作用就像个T*指针。声明迭代器为const就像声明指针为const一样(即声明一个T* const指针),表示迭代器不得指向不同的东西,但它所指东西的值可以改变。如果希望所指东西不可改变(即希望STL模拟一个const T*指针),需要的是const_iterator:

1 std::vector<int> vec;
2 const std::vector<int>::iterator iter = vec.begin();   //iter的作用像个T* const
3 *iter = 10;                                            //没问题,改变iter所指物 
4 ++iter;                                                //错误!iter是const   
5 std::vector<int>::const_iterator cIter = vec.begin();  //cIter的作用像个const T*
6 *cIter = 10;                                           //错误!*cIter是const
7 ++cIter;                                               //没问题,改变cIter  

 

     const最具威力的用法是在函数声明时的应用。一个函数声明式内,const可以和函数返回值、各参数、函数自身(如果是成员函数)产生关联。令函数返回一个常量值,可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。有理数的operator*声明式:

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

返回一个const对象的原因是如果不这样用户就能实现这样的暴行:

1 Rational a, b, c;
2 
3 (a * b) = c;  //在a * b的成果上调用operator=

不知道为什么会有人想对两个数的乘积再做一次赋值,但知道很多程序员无意识中打字错误:

1 if(a * b = c)  //喔欧,其实是想做一个比较动作!

如果a和b是内置类型,这样的代码直截了当就是不合法。一个“良好的用户自定义类型”的特征是它们避免无端地与内置类型不兼容,因此允许对两值乘积做赋值动作也就没有什么意思。将operator*的回传值声明为const可以预防那个“没意思的赋值动作”,这就是该那么做的原因。

     至于const参数,没有什么特别新颖的观念,不过像local const对象一样,你应该在必要使用的时候使用它们。除非你有需要改动的参数或local对象,否则请将它们声明为const。只不过多打6个字,却省下恼人的错误,像是“想要键入‘==’却意外键成‘=’”的错误,一如稍早所述。

 

const成员函数

     将const实施于成员函数的目的,是为了确认该成员函数可用于对const对象身上。这类成员函数重要的两个理由:第一,它们使class接口比较容易理解,得知哪个函数可以改动对象内容而哪个不行,很是重要。第二,使“操作const对象”成为可能。这对编写高效代码是个关键,如条款20所言,改善C++程序效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术可行的前提是,我们有const成员函数可用来处理取得(并经修饰而成)的const对象。

     许多人漠视一件事实:两个成员函数如果只是常量性(constness)不同,可以被重载。这是在是一个重要的C++特性。考虑以下class,用来表现一大块文字:

 1 class TextBlock {
 2 public:
 3   
 4   const char& operator[](std::size_t position) const  //operator[] for const 对象
 5   { return text[position]; }
 6   char& operator[](std::size_t position)              //operator[] for non-const 对象
 7   { return text[position]; }
 8 private:
 9   std::string text;
10 };

TextBlock的operator[]s可以被这么使用:

1 TextBlock tb("Hello");
2 std::cout << tb[0];           //调用non-const TextBlock::operator[]
3 const textBlock ctb("World");
4 std::cout << ctb[0];          //调用const TextBlock::operator[]

附带提一下,真实程序中const对象大多用于passed by pointer-to-const或passed by reference-to-const的传递结果。上述的ctb例子太过造作,下面的比较真实:

1 void print(const TextBlock& ctb)
2 {
3   std::cout << ctb[0];
4   
5 }

只要重载operator[]并对不同的版本给予不同的返回类型,就可以令const和non-const TextBlocks获得不同的处理:

1 std::cout << tb[0];    //没问题-读一个non-const TextBlock
2 tb[0= 'x';           //没问题-写一个non-const TextBlock
3 std::cout << ctb[0];   //没问题-读一个const TextBlock
4 ctb[0= 'x';          //错误!-写一个const TextBlcok

注意,上述错误只因operator[]的返回类型以致,至于operator[]调用动作自身没问题。错误起因于企图对一个“由const版之operator[]返回”的const char&实行赋值动作。

     也请注意,non-const operator[]的返回类型是个reference to char,不是char。如果operator[]只是返回一个char,下面这样的句子就无法通过编译:

1 tb[0= 'x';

那是因为,如果函数的返回类型是一个内置类型,那么改动函数返回值从来就不是合法。纵使合法,C++以by value返回对象这一事实(见条款20)意味着改动的其实是tb.text[0]的一个副本,不是tb.text[0]自身,那不会是你想要的行为。

     让我们为哲学思辨喊一次暂停。成员函数如果是const意味着什么?这有两个流行概念:bitwise constness(又称physical constness)和logical constness。bitwise const阵营的人相信,成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const,也就是说不更改对象内的任何一个bit。该论点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

     不幸的是许多成员函数虽然不十足具备const性质却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针(而非其所指物)隶属于对象,那么称此函数为bitwise const不会引发编译器异议,这导致反直观结果。假设我们有一个TextBlock-like class,它将数据存储为char*而不是string,因为它需要和一个不认识string对象的C API沟通:

1 class CTextBlock {
2 public:
3   
4   char& operator[](std::size_t position) const //bitwise const声明,但其实不恰当
5   { return pText[positon]; }
6 private:
7   char* pText;
8 }

这个class不恰当地将operator[]声明为const成员函数,而该函数却返回一个reference指向对象内部值(条款28对此有深刻讨论)。假设暂时不管这个事实,请注意,operator[]实现代码并不更改pText。于是编译器很开心地的为operator[]产生目标代码。它是bitwise const,所有编译器都这么认定。但看看它允许发生什么:

1 const CtextBlock cctb("Hello"); //声明一个常量对象。
2 char* pc = &cctb[0];            //调用const operator[]取得一个指针,指向cctb的数据。
3 *pc = 'J';                      //cctb现在有了"Jello"这样的内容。

这其中当然不该有任何错误:你创建一个常量对象并设以某值,而且对它调用const成员函数。但你终究还是改变了它的值。

     这种情况导出所谓的logical constness。这派主张一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。例如你的CTextBlock class有可能高速缓存文本区块的长度以便应付询问:

 1 class CTextBlock {
 2 public:
 3   
 4   std::size_t length() const;
 5 private:
 6   char* pText;
 7   std::size_t textLength;  //最近一次计算的文本区块长度。
 8   bool lengthIsValid;  //目前的长度是否有效。
 9 };
10 std::size_t CTextBlock::length() const
11 {
12   if(!lengthIsValid) {
13     textLength = std::strlen(pText);  //错误!在const成员函数内不能赋值给textLength和lengthIsValid。
14     lengthIsValid = true;
15   }
16   return textLength;
17 }

    length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两笔数据被修改对const CTextBlock对象而言虽然可以接受,但编译器不同意。它们坚持bitwise constness,怎么办?解决方法很简单:利用C++的一个与const相关的摆动场:mutable(可变的)。mutable释放掉non-static成员变量的bitwise constness约束:

 1 class CTextBlock {
 2 public:
 3     
 4   std::size_t length() const;
 5 private:
 6   char* pText;
 7   std::size_t textLength;  //这些成员变量可能总是会被改变,即使在const成员函数内。
 8   bool lengthIsValid;
 9 };
10 std::size_t CTextBlock::length() const
11 {
12   if(!lengthIsValid) {
13     textLength = std::strlen(pText);  //现在可以这样,
14     lengthIsValid = true;             //也可以这样。
15   }
16   return textLength;
17 }

 

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

     mutable不能解决所有的const难题。举个例子,假设TextBlock(和CTextBlock)内的operator[]不单只是返回一个reference指向某字符,也执行边界检验、志记访问信息、甚至可能进行数据完整性检验。把所有这些同时放进const和non-const operator[]中,导致这样的怪物:

代码
 1 class TextBlock {
 2 public:
 3   ...
 4   const char& operator[](std::size_t position) const
 5   {
 6     ...//边界检验
 7     ...//志记数据访问
 8     ...//检验数据完整性
 9     return text[position];
10   }
11   char& operator[](std::size_t position)
12   {
13     ...//边界检验
14     ...//志记数据访问
15     ...//检验数据完整性
16     return text[position];
17   }
18 private:
19   std::string text;
20 };

     能说出其中发生的代码重复以及伴随的编译时间、维护、代码膨胀等问题吗?将边界检验移到另一个成员函数(往往是private)并令两个版本的operator[]调用它是可能的,但还是重复了如函数调用、两次return语句等。

     真正该做的是实现operator[]的机能一次并使用它两次,也就是说必须令其中的一个调用另一个。

     一般而言,转型是一个糟糕的想法,而代码重复也不是令人愉快的经验。本例中const operator[]完全做掉了non-const版本该做的一切,唯一的不同是其返回类型多了一个const资格修饰。这种情况下如果将返回值的const转除是安全的,因为不论谁调用non-const operator[]都一定首先有一个non-const对象,否则就不能够调用non-const函数。所以令non-const operator[]调用其const兄弟是一个避免代码重复的安全做法——即使过程中需要一个转型动作。

代码
 1 class TextBlock {
 2 public:
 3   ...
 4   const char& operator[](std::size_t position) const //一如既往
 5   {
 6     ...
 7     ...
 8     ...
 9     return text[position];
10   }
11   char& operator[](std::size_t position)  //现在只调用const op[]
12   {
13     return const_cast<char&>(                 //将op[]返回值的const转除
14       static_cast<const TextBlock&>(*this)    //为*this加上const
15         [position]                            //调用const op[]
16       );
17   }
18   ...
19 };

      如你所见,这份代码有两个转型动作,而不是一个。让non-const operator[]调用其const兄弟,但non-const operator[]内部若只是单纯调用operator[],会递归调用自己。为了避免无穷递归,必须明确指出调用的是const operator[],但C++缺乏直接的语法可以这么做。因此可以将*this从其原始类型TextBlock&转型为const TextBlock&。使用转型操作为它加上const,所以这里共有两次转型:第一次用来为*this添加const(这使接下来调用operator[]时得以调用const版本),第二次则是从const operator[]的返回值中移除const。

     添加const的那一次转型强迫进行了一次安全转型(将non-const对象转为const对象),使用static_cast。移除const的那个动作只可以由const_cast完成,没有其他选择(就技术而言其实是有的;一个C-style转型也行得通,但那种转型很少是正确的选择,如果不熟悉static_cast或const_cast,条款27提供了一份概要)。

     至于其他动作,由于本例调用的是操作符,所以语法有一点点奇特,恐怕无法赢得选美大赛,但却有“避免代码重复”效果,因为运用const operator[]实现出non-const版本。为了达到这个目标而写出如此难看的代码是否值得只有你能决定,但“运用const成员函数实现出其non-const孪生兄弟”的技术是值得了解的。

     更值得了解的是,反向做法——令const版本调用non-const版本以避免重复——并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就是冒了这样的风险:你曾经承诺不改动的那个对象被改动了。这就是为什么“const成员函数调用non-const成员函数”是一种错误的行为:因为对象有可能因此被改动。实际上若要令这样的代码通过编译,你必须使用一个const_cast将*this身上的const性质解放掉,这是乌云罩顶的清晰前兆。反向调用才是安全的:non-const成员函数本来就可以对其对象做任何动作,所以在其中调用一个const成员函数并不会带来风险。这就是为什么本例以static_cast作用于*this的原因:这里并不存在const相关风险。

     本条款一开始就提醒你,const是个奇妙且非比寻常的东西。在指针和迭代器身上;在指针、迭代器及references指涉的对象身上;在函数参数和返回类型身上;在local变量身上;在成员函数身上,林林总总不一而足。const是个威力强大的助手。尽可能使用它,你会对你的作为感到高兴。

请记住

1、将某个东西声明为const可帮助编译器侦测出错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。

2、编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)。

3、当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。

posted @ 2009-11-21 20:30  shengjin  阅读(448)  评论(0编辑  收藏  举报