Skip to content

Latest commit

 

History

History
213 lines (168 loc) · 8.3 KB

File metadata and controls

213 lines (168 loc) · 8.3 KB

条款03:尽可能使用const

const允许你指定一个语义约束(指定一个不该被改动的对象),而编译器会强制实施这项约束。只有某值保持不变是事实,你就该说出来,以获得编译器的帮助,确保这条约束不被违反。

const可以用来修饰global或namespace作用域中的常量,或修饰文件、函数、或区块作用域中被声明为static的对象。你也可以用它修饰类内的static和non-static成员变量。对于指针,你也可以指定指针自身、指针指向的对象,或两者都是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最具威力的用法是面对函数声明时的应用。在一个函数声明内,const可以和返回值,各参数,函数自身(成员函数)产生关联。

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。举个例子,考虑有理数的 operator* 声明式:

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

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

Rational a, b, c;
...
(a * b) = c;  // 在a * b上调用operator=

许多程序员会在无意识中这么做,可能只因为单纯的打字错误(以及一个可被隐式转换为bool的类型):

if (a * b = c) ...  // 其实是想做一个比较

operator* 的返回值声明为const可以预防上述的赋值操作。

至于const参数,它们就像局部const对象一样,你应该在必要使用它们的时候使用它们。除非你有需要改动参数或local对象,否则请将它们声明为const。

const成员函数

将const实施于成员函数的目的,是为了确认该成员函数可作用于const对象上。const成员函数可以使接口比较容易被理解,并使操作const对象成为可能。

两个成员函数如果只是常量性不同,可以被重载。考虑以下class,用来表现一大块文字:

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

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

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

const TextBlock ctb("World");
std::cout << ctb[0];  // const TextBlock::operator[]

const对象大多用于 passed by pointer-to-constpassed by reference-to-const 的传递结果。下面这个比较真实:

void print(const TextBlock& ctb) {
  std::cout << ctb[10];   // const TextBlock::operator[]
  ...
}

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

std::cout << tb[0];   // read only
tb[0] = 'X';          // write only
std::cout << ctb[0];  // read only
ctb[0] = 'Y';         // error! write

错误是因为企图对一个const版本的 operator[] 返回的 const char& 进行赋值。

bitwise constness指成员函数只有在不更改对象的任何成员变量时才可以是const。bitwise constness正是C++对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

许多成员函数虽然不完全具备const性质却能通过bitwise测试。一个更改了指针指向的对象的成员函数虽然不能算是const,但如果只有指针属于对象,那么称此函数为bitwise const不会引发编译器异议。假设我们有一个CTextBlock对象,它将数据存储为char*而不是string:

class CTestBlock {
public:
  ...
  char& operator[](std::size_t position) const  // bitwise const声明
  { return pText[position]; }
private:
  char* pText;
};

这个class不适当地将 operator[] 声明为const成员函数,而该函数却返回一个引用指向对象内部指。请注意,operator[] 实现代码并不更改pText。于是编译器认为是bitwise const。看看它允许发生什么事:

const CTextBlock cctb("Hello");
char* pc = &cctb[0];
*pc = 'J';

这当然没有任何的错误:你创建了一个常对象,只调用了它的常成员函数,但你终究还是改变了它的值。

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

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);  // error
    lengthIsValid = true;             // error
  }
  return textLength;
}

length的实现当然不是bitwise const,因为textLength和lengthIsValid都可能被修改。这两个数据对于const CTextBlock对象而言虽然可接受,但编译器不同意,因为它不是bitwise constness。

利用C++的一个于const相关的摆动场:mutable。mutable释放掉non-static成员变量的bitwise constness约束:

class CTextBlock {
public: 
  ...
  std::size_t length() const;
  mutable std::size_t textLength; // 最近一次计算的文本区块长度
  mutable bool lengthIsValid;     // 目前的长度是否有效
};

std::size_t CTextBlock::length() const {
  if (!lengthIsValid) {
    textLength = std::strlen(pText);  // ok
    lengthIsValid = true;             // ok
  }
  return textLength;
}

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

对于bitwise constness的非我所欲的问题,mutable是个解决办法,但它不能解决所有的const相关难题。假设TextBlock(和CTextBlock)内的 operator[] 不但只是返回一个引用,也执行边界检查、日志访问信息,甚至可能进行数据完善性检验。把所有这些放进const和non-const operator[] 中,导致这样的怪物:

class TextBlock {
public:
  const char& operator[](std::size_t position) const {
    ... // 边界检查
    ... // 日志访问
    ... // 检验数据完整性
    return text[position];
  }
  char& operator[](std::size_t position) {
    ... // 边界检查
    ... // 日志访问
    ... // 检验数据完整性
    return text[position];
  }
private:
  std::string text;
};

你真正该做的是实现 operator[] 的机能一次并使用它两次。也就是说,你必须令其中一个调用另一个。这促使我们将常量性转除。

令non-const operator[] 调用其 const operator[] 是一个避免代码重复的安全做法——即使过程中需要一个转型动作。下面是代码:

class TextBlock {
public:
  const char& operator[](std::size_t position) const {
    ... // 边界检查
    ... // 日志访问
    ... // 检验数据完整性
    return text[position];
  }
  char& operator[](std::size_t position) {
    return const_cast<char&>(               // op[]返回值的const移除
      static_cast<const TextBlock&>(*this)  // 为*this加上const
        [position]                          // 调用const op[]
    );
  }
...
};

值得了解的是,反向做法——令const版本调用non-const版本以避免重复——并不是你该做的事。记住,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。实际上若要令这样的代码通过编译,你必须使用 const_cast*this 的const性质解放掉。

请记住

  • 将某些东西声明为const可帮助编译器侦测错误用法。const可被施加于任何作用域内的对象、函数参数、函数返回类型、成员函数本体。
  • 编译器强制实施bitwise constness,但你编写程序时应该使用概念上的常量性。
  • 当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。