条款02:尽量以const,enum,inline替换#define

  1. 1. 尽可能使用const
    1. 1.1. const成员函数
    2. 1.2. 在const和non-const成员函数中避免重复

用“编译器替换预处理器”应该更好,因为const,enum,inline都是由编译器处理,而#define由预处理器处理,这正是问题所在。当做出这种事情:

#define ASPECT_RADIO 1.653

记号名称ASPECT_RADIO也许从未被编译器所知晓;也许编译器开始处理源代码之前它就被预处理器移走了。于是记号名称ASPECT_RADIO有可能从未进入记号表(symbol table)内。于是,当使用此常量但获得一个编译错误时,可能会带来困惑,因为这个错误信息也许会提到1.653而不是ASPECT_RADIO。这会导致浪费大量时间追踪问题。这个问题也可能出现在记号式调试器(symbolic debugger),原因相同:使用的名称可能并未进入记号表。解决的方法是以一个常量替换上述的宏(#define):

const double AspectRatio = 1.653;

在用常量替换#define,有两种特殊情况需要注意。第一是定义常量指针。由于常量定义式通常被放在头文件内(以便被不同的源码含入),因此有必要将指针(不只是指针所指之物)声明为const。例如若要在头文件内定义一个常量字符串:

const char* const anthorName = "string";// const std::string authorName("string");

第二个值得注意的是class专属常量。为了将常量的作用域限制于class,必须让它成为一个成员;而为确保次常量至多只有一份实体,必须让它成为一个static成员:

1
2
3
4
5
6
class GamePlayer{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
...
};

这里看到的式NumTurns的声明式而非定义式。通常C++要求对使用的任何东西提供一个定义式,但如果是个class专属常量又是static且为整数类型,无需特殊处理。只要不取它们的地址,可以声明并使用它们而无须定义式。但如果要取地址,或这编译器坚持要看到一个定义式,就必须另外提供定义式如下:

const int GamePlayer::NumTurns;

把这个式子放进一个实现文件而非头文件。由于class常量已在声明式种获得初值,因此定义时不可以再设处置

旧式编译器可能不允许static成员在其声明式上获得初值,那么可以将初值放在定义式:

1
2
3
4
5
6
7
8
9
//.h
class GamePlayer{
private:
static const double FudgeFactor;
...
};

//.cpp
const double GamePlayer::FudgeFactor = 1.35;

一般这样做就可以。但是如果class在编译期间需要一个class常量值,例如数组大小。这时候如果编译器不允许为static整数型class常量在声明式中赋初值,可以使用所谓的”the enum hack”补偿做法。其理论基础是:一个属于枚举类型的数值可权充ints被使用。于是可定义:

1
2
3
4
5
6
class GamePlayer{
private:
enum{ NumTurns = 5 };

int scores[NumTurns];
};

enum hack的行为某方面说比较像#define而不像const,有时候这正是想要的。例如取一个const的地址是合法的,但取一个enum的地址就不合法,而取一个#define的地址通常也不合法。

另一个常见的#define误用情况是以它实现宏(macros)。宏看起来像函数,但不会招致函数调用带来的额外开销。下面这个宏夹带着宏实参,调用函数f:

#define CALL_WITH_MAX(a,b) f((a) > (b) ? (a):(b))

无论何时写出这种宏,请记住,必须为宏中的所有实参加上小括号,否则某些人在表达式中调用这个宏时可能会遭遇麻烦。但纵使为所有实参加上小括号,也会有意料外的情况:

1
2
3
int a = 5, b = 0;
CALL_WITH_MAX(++a, b); //a被累加二次
CALL_WITH_MAX(++a, b + 10); //a被累加一次

在调用f之前,a的自增次数取决于“和谁比较”。幸运的是,可以用另一种方法得到宏的效率以及一般函数的可预料行为和类型安全型–template inline函数:

1
2
3
4
5
//不知道T是什么 所以采用 pass by reference-to-const 条款20
template<typename T>
inline void callWidthMax(const T& a,const T& b){
f(a > b ? a : b)
}

虽然const,enum和inline无法完全消除对预处理器的需求,但也有效降低了#define的使用次数。

记住:

  • 对于单纯常量,最好以const对象或者enum代替#define。
  • 对于形似函数的宏,最好改用inline函数替换#define。

尽可能使用const

const允许指定一个语义约束,而编译器会强制实施这项约束。它允许我们告诉编译器和其他程序员某值应该保持不变。只要这(某值保持不变)是事实,你就该确实说出来,因为说出来可以获得编译器的帮助,确保这条约束不被违反。

对于指针,可以指出指针本身,指针所指物,或两者都(或都不)是const:

1
2
3
4
5
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可以和函数返回值,各参数,函数自身(如果式成员函数)产生关联。

令函数返回一个常量值,往往可以降低因客户错误而造成的意外,而又不至于放弃安全性和高效性。比如:

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

为什么要返回一个const对象?原因式如果不这样,客户就可以实现如下写法:

1
2
3
Rational a, b, c;
...
(a * b) = c;

这可能是因为打错或者少打一个符号所致:

if(a * b = c)... //这样的比较动作较为合理

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

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

const成员函数

将const用于成员函数的目的,是为了确认该成员函数可用作于const对象身上。这么做的好处有两个,第一它们使class接口比较容易被理解,明确那个函数可以改动对象内容而哪个函数不行;第二,它们使“操纵const对象”成为可能,这对编写高效代码是个关键。因为如条款20所说,改善C++效率的一个根本办法是以pass by reference-to-const方式传递对象,而此技术的前提是,有const成员函数可以用来处理取得的const对象。

C++有一个重要特性,即两个成员函数如果只是常量性(constness)不同,可以构成重载。看以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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[]可被const和non-const使用:

1
2
3
4
5
TextBlock tb("Hello");
std::cout << tb[0]; //non-const

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

真实程序中const对象大多用于passed by pointer-to-constpassed by reference-to-const的传递结果:

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

成员函数如果是const意味着什么?有两个流行概念:bitwise constness(physical constness)和logical constness。

bitwise constness的人相信,成员函数只有在不更改对象的任何成员变量(static除外)时才可以说是const。也就是说函数不更改对象内的任何一个bit。这种观点的好处是很容易侦测违反点:编译器只需寻找成员变量的赋值动作即可。bitwise constness正是对常量性的定义,因此const成员函数不可以更改对象内任何non-static成员变量。

但是许多成员韩式虽然不具备十足的const特性却能通过bitwise测试。更具体地说,一个更改了“指针所指物”的成员函数虽然不能算是const,但如果只有指针隶属于对象,那么称此函数为bitwise const不会引发编译器异常。

1
2
3
4
5
6
7
8
9
10
class CTextBlock{
public:
char &operator[](std::size_t position) const {
pText[position] = 'Q';
return pText[position];
}

private:
char *pText;
};

此版本没有出现编译上的问题,但也很明显并不符合想象中的bitwise const。这种情况导出所谓的logical constness。这一派主张,一个const成员函数可以修改它所处理的对象内的某些bits,但只有在客户端侦测不出的情况下才得如此。

如果需要在const函数中修改对象本身,可以利用C++的一个与const相关的摆动场:mutable(可变的)。该关键字释放掉non-static成员变量的bitwise constness约束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CTextBlock{
public:
std::size_t length() const {
if (!lengthIsValid){
//现在可以改变了
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}

private:
char *pText;
mutable bool lengthIsValid;
mutable std::size_t textLength;
};

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

以non-const版本调用const版本。

non-const和const版本基本没有不同之处,但仍需要编写两段相同代码。可以通过用其中一个调用另外一个实现实现一次 ,多次使用。这促使我们编码时将常量性移除(casting away constness)。

一般而言,转型是一个糟糕的想法,然而代码重复也很麻烦。计划以非const版本调用const版本函数:

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

char& operator[](std::size_t position) {
return const_cast<char&>( //将返回值的const移除
static_cast<const TextBlock&>(*this)[position] //将*this加上const限定 调用const版本[]
);
}
};

non-const版本的代码有两个转型动作。在non-const operator[]调用其const兄弟,但non-const版本内部若只是单纯调用operator[],会递归调用自己。为了避免递归,必须明确指出调用的是const版本,但C++缺乏直接的语法可以那么做。因此这里将*this从原始类型TextBlock&转型为const TextBlock&,从而调用const版本。第二次转型则是将从const operator[]的返回值中移除const。

值得注意的是,反向做法–令const版本调用non-const版本以避免重复是不可取的。因为,const成员函数承诺绝不改变其对象的逻辑状态,non-const成员函数却没有这般承诺。如果在const函数内调用non-const函数,就会产生这样的风险:你曾经承诺不改变的对象被改动了。想要让这样的代码通过编译,就必须先用const_cast将this的const性质释放掉,这是产生问题的前兆。而反向调用才是安全的:non-const成员函数本来就可以对对象做任何动作,所以在其中调用一个const成员函数并不会带来风险*。

总结:

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