为何C++ Compare命名要求未规定返回支持三态判断的整数值?
很多人第一次接触Compare规范的时候,都会觉得用三态返回值一次比较搞定三种序关系更高效,但这套布尔返回的设计实际上是权衡了易用性、性能、兼容性之后的结果,核心原因有这几点:
实现成本低,出错概率小
要满足Compare要求,只需要实现准确的“小于”逻辑就行,比如给自定义用户类型写排序规则,直接写return a.age < b.age;就符合规范,所有原生的<、>等比较运算符可以直接作为Compare传入,不需要做任何额外包装。如果强制要求返回三态整数,每次写比较逻辑都得分别处理小于、等于、大于三个分支,不仅多写不少样板代码,但凡哪个分支符号写反、返回值写错,直接就会触发std::sort、std::map、std::set等组件的未定义行为,对开发者的要求高了很多。所谓的“两次比较开销”绝大多数场景根本不存在
大家算开销的时候默认每次等价判断都要跑两次比较,但实际上不管是排序的分区、归并逻辑,还是有序容器的插入、查找流程,大部分比较场景根本不需要判断两个元素是不是等价——只需要知道当前元素应该往基准左侧放还是右侧放,这时候布尔比较一次调用就拿到结果了,根本不会触发第二次比较。
就算真的走到等价判断逻辑,!comp(a, b) && !comp(b, a)是短路求值:只要comp(a,b)返回true(也就是a小于b),根本不会执行第二个比较调用,只有当a不小于b的时候才会判断b是不是小于a,两次调用的实际触发占比非常低。
反过来要是强制每次比较都返回三态值,哪怕你只需要知道谁大谁小,函数也得生成对应三种状态的返回值,比如两个整数比大小,CPU比完直接出标志位,转布尔值零额外开销,要转成-1、0、1的整数反而要多跑好几条指令,实际性能更差。标准库已经提前做了针对性优化
对于std::string这种本身就带三态compare方法的类型,标准库的std::less<std::string>等比较谓词是有特化实现的,内部根本不会走两次布尔比较,直接调用三态比较方法拿结果,你担心的额外开销在标准库自带类型上根本不存在。只有用户自定义的、没有提供高效三态比较实现的类型,才会走两次比较的逻辑,而这类类型的比较逻辑往往本身就非常简单,两次调用的开销完全可以忽略。
不少开发者做过实际性能测试,在排序、有序容器增删查等常见场景下,布尔谓词版本的运行效率普遍高于强制三态比较的实现,核心原因就是三态返回值要求每次比较都生成覆盖三种序关系的结果,做了大量场景下完全多余的计算。
- 历史兼容性成本无法承担
这套布尔Compare的设计从STL诞生之初就定下来了,几十年间C生态里有海量的代码都是基于这套约定写的,要是贸然修改Compare的返回值要求,所有现存的自定义比较逻辑都会直接失效,这种破坏性变更的成本是整个生态都承担不起的。就算C20新增了三向比较运算符<=>,标准库也还是完全兼容原来的布尔Compare设计,没有做强制切换。
内容的提问来源于stack exchange,提问作者Cosmo

