第17章 运算符重载

English

1. 运算符重载(yùn suàn fú zhòng zài /yn⁵¹ swan⁵¹ fu³⁵ ʈʂʊŋ⁵¹ tsaɪ⁵¹/)带来了什么,又付出了什么

重载的运算符就是一个名字是标点符号的函数。整个特性就是这样。 a + b 精确地就是 operator+(a, b),a[i] 精确地就是 a.operator[](i),编译器做的事不过是用第二种写法替换第一种 (第17.1章)。

收益是真实的:对一个数值类型来说,a * b + c 读起来就是数学的样子, 没有读者需要去解码某个名字。对一个智能句柄来说,p->member 让包装类型可以用在原本用指针的地方。

代价也是真实的,而且不是你猜的那个代价。它不是多出来的函数调用 —— 那会被内联掉。代价是标点符号不携带任何信息。 + 无法告诉读者它是连接了两个序列、分配了内存、拷贝了十兆字节、 失败了,还是满足结合律。命名函数可以。命名函数还能被文本搜索到, 能放进调试器的断点列表里,也能被一门没有 operator+ 的语言调用。

2. 本章要论证的规则

优先用命名函数。只有当运算符的含义就是这个类型本来就有的含义, 并且这个操作不会失败、不会分配内存、不会让人吃惊时,才重载它。

最典型的例子是序列连接。对两个字符串,a + b 是从 1998 年继承下来的 C++ 习惯,它对字节到底发生了什么什么都没说:它产生一个新字符串, 意味着拷贝 a 的每一个字节和 b 的每一个字节, 意味着一次内存分配,意味着一条错误路径。 concat(a, b) 说的是“把两个序列变成一个”, 而 a.append(b) 说的是“把 b 接到 a 后面, 不产生新值,也不拷贝 a”。三个不同的操作,三种不同的代价, 而用 + 你根本分不清你得到的是哪一个。 第 17.2 章会把这个差别量出来: 同样 4000 字节的结果,用 + 要拷贝 998000 字节,用 append 是 0。

注意这条规则不是什么。它不是“永远不要重载运算符” —— 第17.3章 会给出一个定点数类型, 对它来说 + 和 * 就是正确答案; 第17.4章 会给出正确写法的 == 和 <=>。它是“不要用标点符号去隐藏一个读者必须知道决定”。

3. 一屏看完的对照表

运算符 结论 理由
数值值类型上的 + - * / 可以 这个类型就是算术;不分配、不失败、全域 (第17.3章)
==、<=> 可以 前提是这个类型只有一个显然的相等和一个显然的序 (第17.4章)
+= 这类复合赋值 可以 它是原语;+ 是拿它定义出来的
[] 可以,前提是它便宜且不会失败 容器下标;一旦它能插入或能抛,它就在撒谎 (第17.7章)
*、-> 可以,用于句柄 行为像指针的包装类型
() 可以,用于可调用对象 这是每个接受可调用对象的算法都依赖的契约;无状态时写成 static inline constexpr,并且永远不要当“万能入口”用 (第17.7章第6节)
容器类型上的 + 不行 隐藏了分配、拷贝和失败;用 concat 或 append(第17.2章)
, 永远不行 函数调用无法给它的实参排序,而排序正是逗号运算符唯一要做的事 (第17.8章)
&(一元,取地址) 永远不行 它是 C 指代一个对象五十年的方式,而补救的代价是标准库多了一个 std::addressof (第17.8章第3节)
为 I/O 重载的 <<、>> 永远不行 移位不是打印;iostream 糟糕且有害,这正是 fast_io 有 print 和 scan 的原因 (第17.8章)
operator T() 只有 explicit operator bool 远距离的隐式转换;隐式的那些会静默地改变 == 和 + 的含义 (第17.6章)
++、-- 可以,前缀优先 前缀形式是原语;后缀是三行代码加一次拷贝 (第17.5章)
operator new / operator delete 永远不行 整张账单在 第18.7章

4. 目录

5. 读这一章之前你需要什么