Effective C++手册Item 3: 只要可能就用 const

Item 3: 只要可能就用 const

作者:Scott Meyers

译者:fatalerror99 (iTePub's Nirvana)

发布:http://blog.csdn.net/fatalerror99/

关于 const 的一件美妙的事情是它允许你指定一种 semantic(语义上的)约束:一个特定的 object(对象)不应该被修改。而 compilers(编译器)将执行这一约束。它允许你通知 compilers(编译器)和其他程序员,某个值应该保持不变。如果确实如此,你就应该明确地表示出来,因为这样一来,你就可以谋求 compilers(编译器)的帮助,确保这个值不会被改变。

keyword(关键字)const 非常多才多艺。在 classes(类)的外部,你可以将它用于 global(全局)或 namespace(命名空间)范围的 constants(常量)(参见 Item 2),以及那些在 file(文件)、function(函数)或 block(模块)scope(范围)内被声明为 static(静态)的对象。在 classes(类)的内部,你可以将它用于 static(静态)和 non-static(非静态)data members(数据成员)上。对于 pointers(指针),你可以指定这个 pointer(指针)本身是 const,或者它所指向的数据是 const,或者两者都是,或者都不是:

char greeting[] = "Hello";
char *p = greeting; // non-const pointer,
                    // non-const data
const char *p = greeting; // non-const pointer,
                          // const data
char * const p = greeting; // const pointer,
                           // non-const data
const char * const p = greeting; // const pointer,
                                 // const data

这样的语法本身其实并不像表面上那样反复无常。如果 const 出现在星号左边,则指针 pointed to(指向)的内容为 constant(常量);如果 const 出现在星号右边,则 pointer itself(指针自身)为 constant(常量);如果 const 出现在星号两边,则两者都为 constant(常量)。

当指针指向的内容为 constant(常量)时,一些人将 const 放在类型之前,另一些人将它放在类型之后星号之前。两者在意义上并没有区别,所以,如下两个函数具有相同的 parameter type(参数类型):

void f1(const Widget *pw); // f1 takes a pointer to a
                           // constant Widget object
void f2(Widget const *pw); // so does f2

因为它们都存在于实际的代码中,你应该习惯于这两种形式。

STL iterators(迭代器)以 pointers(指针)为原型,所以一个 iterator 在行为上非常类似于一个 T* pointer(指针)。声明一个 iterator 为 const 就类似于声明一个 pointer(指针)为 const(也就是说,声明一个 T* const pointer(指针)):不能将这个 iterator 指向另外一件不同的东西,但是它所指向的东西本身可以变化。如果你要一个 iterator 指向一个不能变化的东西(也就是一个 const T* pointer(指针)的 STL 对等物),你需要一个 const_iterator:

std::vector vec;
...
const std::vector::iterator iter =     // iter acts like a T* const
  vec.begin();
*iter = 10;                                 // OK, changes what iter points to
++iter;                                     // error! iter is const
std::vector::const_iterator cIter =    // cIter acts like a const T*
  vec.begin();
*cIter = 10;                                // error! *cIter is const
++cIter;                                    // fine, changes cIter

对 const 最强有力的用法来自于它在 function declarations(函数声明)中的应用。在一个 function declaration(函数声明)中,const 既可以用在函数的 return value(返回值)上,也可以用在个别的 parameters(参数)上,对于 member functions(成员函数),还可以用于整个函数。

一个函数返回一个 constant value(常量值),常常可以在不放弃安全和效率的前提下尽可能减少客户的错误造成的影响。例如,考虑在 Item 24 中考察的 rational numbers(有理数)的 operator* 函数的声明。

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

很多第一次看到这些的程序员会不以为然。为什么 operator* 的结果应该是一个 const object(对象)?因为如果它不是,客户就可以犯下如此暴行:

Rational a, b, c;
...
(a * b) = c; // invoke operator= on the
             // result of a*b!

我不知道为什么一些程序员要为两个数的乘积赋值,但是我知道很多程序员这样做也并非不称职。所有这些可能来自一个简单的输入错误(要求这个类型能够隐式转型到 bool):

if (a * b = c) ... // oops, meant to do a comparison!

如果 a 和 b 是 built-in type(内建类型),这样的代码显而易见是非法的。一个好的 user-defined types(用户自定义类型)的特点就是要避免与 built-ins(内建类型)毫无理由的不和谐(参见 Item 18),而且对我来说允许给两个数的乘积赋值看上去正是毫无理由的。将 operator* 的返回值声明为 const 就可以避免这一点,这就是我们要这样做的理由。

关于 const parameters(参数)没什么特别新鲜之处——它们的行为就像 local(局部)的 const objects(对象),而且无论何时,只要你能,你就应该这样使用。除非你需要改变一个 parameter(参数)或 local object(本地对象)的能力,否则,确保将它声明为 const。它只需要你键入六个字符,就能将你从我们刚刚看到的这个恼人的错误中拯救出来:“我想键入 '==',但我意外地键入了 '='”。

const member functions(const 成员函数)

member functions(成员函数)被声明为 const 的目的是标明这个 member functions(成员函数)可能会被 const objects(对象)调用。因为两个原因,这样的 member functions(成员函数)非常重要。首先,它使一个 class(类)的 interface(接口)更容易被理解。知道哪个函数可以改变 object(对象)而哪个不可以是很重要的。第二,它们可以和 const objects(对象)一起工作。因为,书写高效代码有一个很重要的方面,就像 Item 20 所解释的,提升一个 C++ 程序的性能的基本方法就是 pass objects by reference-to-const(以传引用给 const 的方式传递一个对象)。这个技术只有在 const member functions(成员函数)和作为操作结果的 const-qualified objects(被 const 修饰的对象)存在时才是可行的。

很多人没有注意到这样的事实,即 member functions(成员函数)在只有 constness(常量性)不同时是可以被 overloaded(重载)的,但这是 C++ 的一个重要特性。考虑一个代表文本块的类:

class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const   // operator[] for
  { return text[position]; }                           // const objects
  char& operator[](std::size_t position)               // operator[] for
  { return text[position]; }                           // non-const objects
private:
   std::string text;
};

TextBlock 的 operator[]s 可能会这样使用:

TextBlock tb("Hello");
std::cout << tb[0];                    // calls non-const
                                       // TextBlock::operator[]
const TextBlock ctb("World");
std::cout << ctb[0];                   // calls const TextBlock::operator[]

顺便提一下,const objects(对象)在实际程序中最经常出现的是作为这样一个操作的结果:passed by pointer- or reference-to-const(以传指针或者引用给 const 的方式传递)。上面的 ctb 的例子是人工假造的。下面这个例子更真实一些:

void print(const TextBlock& ctb)       // in this function, ctb is const
{
  std::cout << ctb[0];                 // calls const TextBlock::operator[]
  ...
}

通过 overloading(重载) operator[],而且给不同的版本不同的返回类型,你能对 const 和 non-const 的 TextBlocks 做不同的操作:

std::cout << tb[0];                    // fine — reading a
                                       // non-const TextBlock
tb[0] = 'x';                           // fine — writing a
                                       // non-const TextBlock
std::cout << ctb[0];                   // fine — reading a
                                       // const TextBlock
ctb[0] = 'x';                          // error! — writing a
                                       // const TextBlock

请注意这里的错误只与被调用的 operator[] 的 return type(返回类型)有关,而调用 operator[] 本身总是正确的。错误出现在企图为 const char& 赋值的时候,因为它是 const 版本的 operator[] 的 return type(返回类型)。

再请注意 non-const 版本的 operator[] 的 return type(返回类型)是 reference to a char(一个 char 的引用)而不是一个 char 本身。如果 operator[] 只是返回一个简单的 char,下面的语句将无法编译:

tb[0] = 'x';

因为改变一个返回 built-in type(内建类型)的函数的返回值总是非法的。即使它合法,C++ returns objects by value(以传值方式返回对象)这一事实(参见 Item 20)也意味着被改变的是 tb.text[0] 的一个 copy(拷贝),而不是 tb.text[0] 自己,这不会是你想要的行为。

让我们为哲学留一点时间。看看一个 member function(成员函数)是 const 意味着什么?有两个主要的概念:bitwise constness(二进制位常量性)(也称为 physical constness(物理常量性))和 logical constness(逻辑常量性)。

bitwise(二进制位)const 派别坚持认为,一个 member function(成员函数),当且仅当它不改变 object(对象)的任何 data members(数据成员)(static(静态的)除外),也就是说如果不改变 object(对象)内的任何 bits(二进制位),则这个 member function(成员函数)就是 const。bitwise constness(二进制位常量性)的一个好处是比较容易监测违例:编译器只需要寻找对 data members(数据成员)的 assignments(赋值)。实际上,bitwise constness(二进制位常量性)就是 C++ 对 constness(常量性)的定义,一个 const member function(成员函数)不被允许改变调用它的 object(对象)的任何 non-static data members(非静态数据成员)。

不幸的是,很多效果上并不是完全 const 的 member functions(成员函数)通过了 bitwise(二进制位)的检验。特别是,一个经常改变某个 pointer(指针)指向的内容的 member function(成员函数)效果上不是 const 的。除非这个 pointer(指针)在这个 object(对象)中,否则这个函数就是 bitwise(二进制位)const 的,编译器也不会提出异议。例如,假设我们有一个 TextBlock-like class(类似 TextBlock 的类),因为它需要与一个不知 string objects(对象)为何物的 C API 打交道,所以它需要将它的数据存储为 char* 而不是 string。

class CTextBlock {
public:
  ...
  char& operator[](std::size_t position) const   // inappropriate (but bitwise
  { return pText[position]; }                    // const) declaration of
                                                 // operator[]
private:
  char *pText;
};

尽管 operator[] 返回 a reference to the object's internal data(一个引向对象内部数据的引用),这个 class(类)还是(不适当地)将它声明为一个 const member function(成员函数)(Item 28 将谈论一个深入的主题)。先将它放到一边,看看 operator[] 的实现,它并没有使用任何手段改变 pText。结果,编译器愉快地生成了 operator[] 的代码,因为毕竟对所有编译器而言,它都是 bitwise(二进制位)const 的,但是我们看看会发生什么:

const CTextBlock cctb("Hello");        // declare constant object
char *pc = &cctb[0];                   // call the const operator[] to get a
                                       // pointer to cctb's data
*pc = 'J';                             // cctb now has the value "Jello"

这里确实出了问题,你用一个 particular value(确定值)创建一个 constant object(常量对象),然后你只是用它调用了 const member functions(成员函数),但是你还是改变了它的值!

这就引出了 logical constness(逻辑常量性)的概念。这一理论的信徒认为:一个 const member function(成员函数)可能会改变调用它的 object(对象)中的一些 bits(二进制位),但是只能用客户无法察觉的方法。例如,你的 CTextBlock class(类)在需要的时候可以储存文本块的长度:

class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  std::size_t textLength;             // last calculated length of textblock
  bool lengthIsValid;                 // whether length is currently valid
};
std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);  // error! can't assign to textLength
    lengthIsValid = true;             // and lengthIsValid in a const
  }                                   // member function
  return textLength;
}

length 的实现当然不是 bitwise(二进制位)const 的—— textLength 和 lengthIsValid 都可能会被改变——但是它还是被看作对 const CTextBlock 对象有效。但编译器不同意,它还是坚持 bitwise constness(二进制位常量性),怎么办呢?

解决方法很简单:利用以 mutable 闻名的 C++ 的 const-related(const 相关)的灵活空间。mutable 将 non-static data members(非静态数据成员)从 bitwise constness(二进制位常量性)的约束中解放出来:

class CTextBlock {
public:
  ...
  std::size_t length() const;
private:
  char *pText;
  mutable std::size_t textLength;         // these data members may
  mutable bool lengthIsValid;             // always be modified, even in
};                                        // const member functions
std::size_t CTextBlock::length() const
{
  if (!lengthIsValid) {
    textLength = std::strlen(pText);      // now fine
    lengthIsValid = true;                 // also fine
  }
  return textLength;
}

避免 const 和 non-const member functions(成员函数)的重复

mutable 对于解决 bitwise-constness-is-not-what-I-had-in-mind(二进制位常量性不太合我的心意)的问题是一个不错的解决方案,但它不能解决全部的 const-related(const 相关)难题。例如,假设 TextBlock(包括 CTextBlock)中的 operator[] 不仅要返回一个适当的字符的 reference(引用),它还要进行 bounds checking(边界检查),logged access information(记录访问信息),甚至 data integrity validation(数据完整性确认),将这些功能都加入到 const 和 non-const 的 operator[] 函数中(不必为我们现在有着非凡长度的 implicitly inline functions(隐含内联函数)而烦恼,参见 Item 30),使它们变成如下这样的庞然大物:

class TextBlock {
public:
  ...
  const char& operator[](std::size_t position) const
  {
    ...                                 // do bounds checking
    ...                                 // log access data
    ...                                 // verify data integrity
    return text[position];
  }
  char& operator[](std::size_t position)
  {
    ...                                 // do bounds checking
    ...                                 // log access data
    ...                                 // verify data integrity
    return text[position];
  }
private:
   std::string text;
};

哎呀!你是说 code duplication(重复代码)?还有随之而来的额外的编译时间,维护成本以及代码膨胀等令人头痛之类的事情吗?当然,也可以将 bounds checking(边界检查)等全部代码转移到一个单独的 member function(成员函数)(自然是 private(私有)的)中,并让两个版本的 operator[] 来调用它,但是,你还是要重复写出调用那个函数和 return 语句的代码。

你真正要做的是只实现一次 operator[] 的功能,而使用两次。换句话说,你可以用一个版本的 operator[] 去调用另一个版本。并可以为我们 casting away(通过强制转型脱掉)constness(常量性)。

作为一个通用规则,casting(强制转型)是一个非常坏的主意,我会投入整个一个 Item 的篇幅来告诉你不要使用它(Item 27),但是 code duplication(重复代码)也不是什么好事。在当前情况下,const 版本的 operator[] 所做的事也正是 non-const 版本所做的,仅有的不同是它有一个 const-qualified return type(被 const 修饰的返回类型)。在这种情况下,casting away(通过强制转型脱掉)return value(返回类型)的 const 是安全的,因为,无论谁调用 non-const operator[],首先要有一个 non-const object(对象)。否则,它不能调用一个 non-const 函数。所以,即使需要一个 cast(强制转型),让 non-const operator[] 调用 const 版本也是避免重复代码的安全方法。代码如下,你读了后面的解释后对它的理解可能会更加清晰:

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(                         // cast away const on
                                                 // op[]'s return type;
        static_cast(*this)     // add const to *this's type;
          [position]                             // call const version of op[]
      );
  }
...
};

正如你看到的,代码中有两处 casts(强制转型),而不是一处。我们让 non-const operator[] 调用 const 版本,但是,如果在 non-const operator[] 的内部,我们仅仅是调用 operator[],那我们将递归调用我们自己。它会进行一百万次甚至更多。为了避免 infinite recursion(无限递归),我们必须明确指出我们要调用 const operator[],但是没有直接的办法能做到这一点,于是我们将 this 从 TextBlock& 的自然类型强制转型到 const TextBlock&。是的,我们使用 cast(强制转型)为它加上了 const!所以我们有两次 casts(强制转型):第一次是为 this 加上 const(以便在我们调用 operator[] 时调用它的 const 版本),第二次是从 const operator[] 的 return value(返回值)之中去掉 const。

加上 const 的 cast(强制转型)仅仅是强制施加一次安全的转换(从一个 non-const object(对象)到一个 const object(对象)),所以我们用一个 static_cast 来做。去掉 const 只能经由 const_cast 来完成,所以在这里我们没有别的选择。(在技术上,我们有。一个 C-style cast(C 风格的强制转型)也能工作,但是,就像我在 Item 27 中解释的,这样的 casts(强制转型)很少是一个正确的选择。如果你不熟悉 static_cast 或 const_cast,Item 27 中包含有一个概述。)

在完成其它事情的基础上,我们在此例中调用了一个 operator(操作符),所以,语法看上去有些奇怪。导致其不会赢得选美比赛,但是它根据 const 版本的 operator[] 实现其 non-const 版本而避免 code duplication(代码重复)的方法达到了预期的效果。使用丑陋的语法达到目标是否值得最好由你自己决定,但是这种根据 const member function(成员函数)实现它的 non-const 版本的技术却非常值得掌握。

更加值得掌握的是做这件事的反向方法——通过用 const 版本调用 non-const 版本来避免重复——是你不能做的。记住,一个 const member function(成员函数)承诺绝不会改变它的 object(对象)的逻辑状态,但是一个 non-const member function(成员函数)不会做这样的承诺。如果你从一个 const member function(成员函数)调用一个 non-const member function(成员函数),你将面临你承诺不会变化的 object(对象)被改变的风险。这就是为什么使用一个 const member function(成员函数)调用一个 non-const member function(成员函数)是错误的,object(对象)可能会被改变。实际上,那样的代码如果想通过编译,你必须用一个 const_cast 来去掉 this 的 const,这是一个显而易见的麻烦。而反向的调用——就像我在上面用的——是安全的:一个 non-const member function(成员函数)对一个 object(对象)能够为所欲为,所以调用一个 const member function(成员函数)也没有任何风险。这就是为什么 static_cast 在这种情况下可以工作在 this 上的原因:这里没有 const-related 危险。

就像在本 Item 开始我所说的,const 是一件美妙的东西。在 pointers(指针)和 iterators(迭代器)上,在 pointers(指针),iterators(迭代器)和 references(引用)涉及到的 object(对象)上,在 function parameters(函数参数)和 return types(返回值)上,在 local variables(局部变量)上,在 member functions(成员函数)上,const 是一个强有力的盟友。只要可能就用它,你会为你所做的感到高兴。

Things to Remember

  • 将某些东西声明为 const 有助于编译器发现使用错误。const 能被用于任何 scope(范围)中的 object(对象),用于 function parameters(函数参数)和 return types(返回类型),用于整个 member functions(成员函数)。

  • 编译器坚持 bitwise constness(二进制位常量性),但是你应该用 conceptual constness(概念上的常量性)来编程。(此处原文有误,conceptual constness为作者在本书第二版中对 logical constness 的称呼,正文中的称呼改了,此处却没有改。其实此处还是作者新加的部分,却使用了旧的术语,怪!——译者注)

  • 当 const 和 non-const member functions(成员函数)具有本质上相同的实现的时候,使用 non-const 版本调用 const 版本可以避免 code duplication(代码重复)。