0%

[Effective C++ 笔记]02. 尽量以 const,enum,inline 替换 #define

简介

本条款中介绍了替代预处理指令 #define 的几种做法。

  • 对于常量,最好以 const 对象或 enum 来替换 #define
  • 对于形似函数的宏 (macros),最好改用 inline 函数替换 #define

例子

这个条款还可以有另一种说法:“尽量使用编译器操作代替预处理器操作”,使用 #define 的问题在于:#define 本身不被视为语言的一部分,参考以下例子:

当我们使用:

1
#define ASPECT_RATIO 1.653

由于在预处理阶段,所有 ASPECT_RATIO 都被替换成了 1.653。所以对于编译器而言,ASPECT_RATIO 相当于从没出现过,也不会保存在其记号表 (symbol table)里。这对于我们在 debug 过程中,有可能会带来很多困难。当我们因为使用了常量而编译错误时,编译错误信息有可能会出现 1.653 但不会出现 ASPECT_RATIO;而如果这个常量是定义在某个不是我们写的头文件(第三方库)中,追踪错误信息会很困难。原因就是因为记号表里没有 ASPECT_RATIO

而针对上述情况,解决的方法是用一个常量来代替宏:

1
const double AspectRatio = 1.653;   // 常量一般不使用全大写

作为常量,AspectRatio 会进入编译器的记号表中。另外当我们使用常量的类型很长时(如本例的浮点数常量),在多个地方使用宏(ASPECT_RATIO)会导致目标代码中重复出现多次该常量(本例是 1.653 ),因此提高了空间消耗。

具体实施方法

接下来是是使用常量替换 #define 的一些具体做法,这里主要提以下两种情况:

定义常量指针

由于常量定义通常会放在头文件中,所以当我们定义常量指针时,除了定义指针对象为 const 以外,还需要将指针本身也定义成 const,以防止别人使用时修改指针本身使其不再指向原对象。例如我们要定义一个常量字符串(以 char* 的形式时),需要定义成:

1
const char* const authorName = "Scott Meyers";

关于 const 的使用会在之后的条款中讨论。当然,针对字符串而言,使用 std::stringchar* 更加方便,所以上述定义也可以改写为:

1
const std::string authorName("Scott Meyers");

定义 class 专属常量

使用 const static 常量

首先要说明一点,由于宏定义 #define 并不关于作用域 (scope)。只要我们使用了某个宏定义,在之后编译中只要没出现(#undef)所以出现了宏定义的地方都会有效。因此 #define 不能够用来定义 class 专属常量,也不能提供任何封装性。而使用 const 常量是可以的。

在使用 const 来定义 class 专属常量时,需要满足两点:

  1. 为了将其作用域限制在 class 中,需要让其成为该 class 的一个成员变量。
  2. 为了这个常量针对所有实例都只保存一份实体,需要将其定义成一个 static 成员

如下所示:

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

这里要注意一下,上述中 NumTurns 的语句只是一条声明式而非定义式。意味着编译器并没有对其分配空间,因此没有地址。理论上不能使用,但是假如它满足:

  1. 属于 class 专属变量
  2. 并且是 static 的注意:C++11 之后的标准已经支持 non-static 变量在声明式中获得初值,参考:In-class member initializers
  3. 而且是整数类型(integral type,如 int, char, bool

满足以上条件的情况下,只要我们在不对其取地址的情况下,只需要声明就可以使用它。如果我们需要获取该变量 (NumTurns)的地址(或者某些编译器坚持需要一个定义式)时,我们可以在实现文件中提供定义式如下:

1
const int GamePlayer::NumTurns; // NumTurns 的定义式,注意由于声明式已提供初值,这里不能再赋值

由于声明式中,NumTurns 已经获得初值,所以定义式中不需要(由于 const 的特性所以也不允许)再次赋值。

注意,这个做法并不是通用的。部分旧式编译器不支持 static 变量在声明式中获得初值;此外,这种 in-class 的初值设定也对整数变量进行。在这种情况下,我们可以将初值放在定义式中:

1
2
3
4
5
6
7
8
// 头文件中
class CostEstimate {
private:
static const double FudgeFactor; // static class 常量声明
};

// 实现文件中
const double CostEstimate::FudgeFactor = 1.35; // static class 常量定义

使用 enum hack

但是在考虑如下情况,我们必须要在声明式中获得初值(如上例中数组 scores 必须要在编译期间就知道数组长度),但是编译器(错误地)不支持 in class initializer 特性。(注:在现在的情况下(2020年),几乎没有编译器不支持这一特性了。) 这种情况下,考虑枚举类型(enum)可以作为 int 使用,我们可以利用一种 “enum hack” 的技巧来实现,如下所示:

1
2
3
4
5
class GamePlayer {
private:
enum { NumTurns = 5 }; // "enum hack" - 将 NumTurns 作为 5 的一个记号名称
int scores[NumTurns]; // 编译成功
}

enum hack 有以下特点使得我们必须对其有一定认识:

  1. enum hack 的行为某种程度上更贴近 #define 而非 const,有时候我们会跟想要这类特性。如:

    • 我们不能获取 enum 的地址。当我们不想让别人通过 pointer 或 reference 来指向你的某个整数变量时,可以使用;
    • 此外,虽然大部分编译器不会为整数型 const 对象设定额外的存储空间;但部分编译器可能会,这样就造成了额外的空间浪费,在这一点上使用 enum#define 都可以避免出现这种情况
  2. 从功利主义的思想来看,很多代码都会使用 enum,所以我们也必须要去学会如何正确使用它。

除了上述使用 #define 来定义常量情况下,还有一种常见的做法是使用 #define 来实现宏(macros)。宏看上去像函数,但又不会有调用函数(function call)时带来的额外开销。下面的例子中宏夹带红参数并调用函数 f

1
2
// 以 a 和 b 的较大值调用 f
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

这种做法看似没有问题,实际上有很多缺点,使用起来相当不方便。如下:

  • 首先是你必须手动地为所有实参(上例中的 ab)添加上小括号,否则在不同情况下(含表达式时)调用宏的时候会出现问题
  • 即便所有实参已经加上小括号了,还是有可能会出现问题。下面的例子中,a 累加的次数取决于 ab 的大小关系。
1
2
3
4
5
#define CALL_WITH_MAX(a, b) f((a) > (b) ? (a) : (b))

int a = 5, b = 0;
CALL_WITH_MAX(++a, b); // a 累加一次
CALL_WITH_MAX(++a, b + 10); // a 累加两次

而当我们想避免出现上述问题,获得一般函数中所有可预料行为已经类型安全,并且想获得宏带来的效率时,我们可以使用模板内联函数(template inline function),如下所示:

1
2
3
4
template<typename T>
inline void callWithMax(const T& a, const T& b) { // 由于我们不知道 T 是什么,所以采用 pass by reference to const
f(a > b ? a : b);
}

上例中的 template 为不同类型对象生成一系列函数。每个函数接受两个同型对象,并以较大值调用函数 f。这个函数除了避免了上述宏中出现的所有问题(不需要手动加小括号,也不用担心表达式会调用多次),并且由于它是一个真正的函数(相对于宏而言),所以它遵守作用域和访问规则。这意味着我们可以写出一个 class 内的 private inline 函数,而宏无法实现这种需求。

总结

利用 constenuminline 的情况下,我们对预处理器(尤指 #define)的需求可以大大降低。当然针对 #include#ifdef/#ifndef 这类指令我们还是会需要使用。总的来说,本条款可以归纳如下:

  • 对于常量,最好以 const 对象或 enum 来替换 #define
  • 对于形似函数的宏 (macros),最好改用 inline 函数替换 #define

完整可运行代码地址:Effective-Cpp-Reading-Note