条款 6:区别 increment/decrement 操作符的前置(prefix)和后置(postfix)形式
很久很久以前(大约 20世纪 80年代后期),在一个遥远的语言(我是指当时的C++)中,没有什么办法可以区分++和—操作符的前置式(prefix)和后置式(postfix)。但程序员毕竟是程序员,他们动不动就对此情况发个牢骚,于是 C++决定扩充,允许++和--操作符的两种形式(前置式与后置式)拥有重载能力。
这时候出现了一个语法上的问题:重载函数是以其参数类型来区分彼此的,然而不论 increment 或 decrement 操作符的前置式或后置式,都没有参数。为了填平这个语言学上的漏洞,只好让后置式有一个 int 自变量,并且在它被调用时,编译器默默地为该 int 指定一个 0 值:
这样的规则或许有点怪异,但你很快就会习惯。重要的是,那些操作符的前置式和后置式返回不同的类型,前置式返回一个 reference,后置式返回一个 const对象。以下我集中讨论++操作符的前置式和后置式,至于--操作符,故事一样。
从 C 的时代回忆起,你或许还记得所谓 increment 操作符的前置式意义“increment and fetch”(累加然后取出),后置式意义“fetch and increment”(取出然后累加)。这两个词组值得记下来,因为它们几乎成为前置式和后置式 increment操作符应该如何实现的正式规范:
请注意后置式操作符并未动用其参数。是的,其参数的唯一目的只是为了区别前置式和后置式而已。如果你在函数体内没有使用函数的命名参数,许多编译器会对此发出警告(见条款 E48),可能会让你觉得烦。为了避免这类警告,一种常见的策略就是故意略去你不打算使用的参数的名称,这正是以上代码实行的策略。
为什么后置式 increment 操作符必须返回一个对象(代表旧值),原因很清楚。但为什么是个 const 对象呢?想象一下,如果不这样,以下动作是合法的:
这就拨云见日了:operator++的第二个调用动作施行于第一个调用动作的返回对象身上。
两个理由使我们不欢迎这样的情况。第一,它和内建类型的行为不一致。设计classes 的一条无上宝典就是:一旦有疑虑,试看 ints 行为如何并遵循之。我们知道,ints 并不允许连续两次使用后置式 increment 操作符:
第二个理由是,即使能够两次施行后置式 increment 操作符,其行为也非你所预期。一如上述所示,第二个 operator++所改变的对象是第一个 operator++返回的对象,而不是原对象。因此即使下式合法:
i 也只被累加一次而已。这是违反直觉的,也容易引起混淆(不论是对 ints 或UPInts),所以最好的办法就是禁止它合法化。
C++针对 ints 禁止了上述行为,而你则必须针对你所设计的 classes 自行动手加以禁止。最简单的做法就是让后置式 increment 操作符返回一个 const 对象。于是当编译器看到:
它便认知到,第一次调用 operator++所返回的 const 对象,将被用来进行operator++的第二次调用。然而 operator++是个 non-const member function,所以 const 对象(亦即本例的后置式 operator++返回值)无法调用之。但是,不执行这项限制的编译器也时有所闻。如果你赖此性质撰写程序,请先测试你的编译器以确定它有正确的行为。如果你曾困惑“令函数返回 const 对象是否合理”,现在你知道了:有时候的确需要如此,后置式 increment 和 decrement 操作符就是个例子(其他例子请见条款 E21)。
如果你很担心效率问题,当你初次看到后置式 increment 函数,或许会头冒冷汗。该函数必须产生一个临时对象,作为返回值之用(见条款 19)。上述实现码也的确产生了一个明显的临时对象(oldValue),需要构造也需要析构。前置式increment 函数就没有如此的临时对象。这导致一个令人吃惊的结论,单以效率因素而言,UPInt 的用户应该喜欢前置式 increment 多过喜欢后置式 increment,除非他们真的需要后置式 increment 的行为。让我们把话说清楚,处理用户定制类型时,应该尽可能使用前置式 increment,因为它天生体质较佳。
现在让我们对 increment 操作符的前置式和后置式做更进一步观察。除了返回值之外,它们做相同的事情:将某值累加。那么,你如何确定后置式 increment 的行为与前置式 increment 的行为一致?你如何保证它们的实现码不会因时间而分道扬镳?说不定不同的程序员对它们分别做了不同的维护与强化。除非遵照上述代码所表现的设计原则,否则你将毫无保障。那个原则是:后置式 increment 和decrement 操作符的实现应以其前置式兄弟为基础。如此一来你就只需维护前置式版本,因为后置式版本会自动调整为一致的行为。
如你所见,掌握 increment 和 decrement 操作符的前置式和后置式是很容易的。一旦你知道它们应该返回什么类型,以及后置式操作符应以前置式操作符为实现基础,就几乎没有什么更高阶的知识需要学习了。