stl的关联容器(map,set)的key一般要求提供 < 比较操作。假设我们有一个结构SomeKey:
struct SomeKey
{
int a, b;
};
要想以SomeKey作为std::map的key,需要为这个结构提供operator <
比较操作,比如:
// 实现1
bool operator < (const SomeKey& left, const SomeKey& right)
{
if (left.a < right.a) // 主key
{
return true;
}
else if (left.a == right.a && left.b < right.b) // 次key
{
return true;
}
else
{
return false;
}
}
或者:
// 实现2
bool operator < (const SomeKey& left, const SomeKey& right)
{
if (left.a != right.a) // 主key
{
return left.a < right.a;
}
if (left.b != right.b) // 次key
{
return left.b < right.b;
}
return false;
}
这两种实现方式是很常见的了,似乎也没什么好聊的。不过在项目中,我一次又一次地遇到错误的operator <
实现,着实让人吃惊!比如:
// 实现3(错误)
bool operator < (const SomeKey& left, const SomeKey& right)
{
if (left.a < right.a) // 主key
{
return true;
}
if (left.b < right.b) // 次key
{
return true;
}
return false;
}
或者
// 实现4(错误)
bool operator < (const SomeKey& left, const SomeKey& right)
{
return (left.a < right.a || left.b < right.b);
}
这样的实现将导致荒谬的结果!比如有两个SomeKey对象:x {10, 20} 和 y {5, 30},采用“实现3”或“实现4”来比较 x 和 y 的大小,结果取决于比较的时候谁在前面,也就是说,如果这样比较:x < y
,结果为真;而这样:y < x
,结果也为真!似乎作者并没有理解 “ < ” 比较操作的含义。这种错误的比较操作,会导致std::map表现出怪异的行为:
std::map<SomeKey, int> m;
m.insert({ { 10, 20 }, 1 });
m.insert({ { 5, 30 }, 2 });
auto it = m.find({ 5, 30 });
if (it == m.end())
{
std::cout << "找不到{5, 30}" << std::endl;
}
else
{
std::cout << "找到{5, 30}" << std::endl;
}
上面的代码片段会输出“找不到{5, 30}”。在向map表insert键值对时,是以被插入值(后称目标值)和红黑树上的节点(后称节点值)比较:
而在map表中find时,是以节点值和目标值进行比较:
因为{10, 20} < {5, 30},所以在{10, 20}的右子树上查找,自然就找不到了。再向map表中插入一个节点,行为就更怪异了:
m.insert({ { 8, 25 }, 3 });
it = m.find({ 5, 30 });
if (it == m.end())
{
std::cout << "找不到{5, 30}" << std::endl;
}
else
{
std::cout << "找到{5, 30}" << std::endl;
}
这次将输出“找到{5, 30}”。因为{8, 25} < {10, 20},继续{8, 25} < {5, 30},最终{8, 25}插入到map表的最左子节点,破坏了红黑树的平衡,右旋后,整棵树变成:
然后在表中find {5, 30},首先找到第一个不小于{5, 30}的节点,因为第一个节点值{5, 30} 不小于目标值{5, 30},而目标值{5, 30}也不小于该节点值{5, 30},于是就找到了目标值。这种怪异行为导致的bug是比较隐秘的:你在表中找一个值,时而找得到,时而又找不到。一般的产品代码中,map表中插入的对象数量都不在少数,你讨厌对大量数据的插入进行测试,你不会认为你的operator <
区区几行代码会有bug,你又不可能怀疑map有什么问题,于是你很可能最终会将之归结于灵异现象。
在产品代码中,结构体可能会更复杂,类似的operator <
错误实现也不少见。
本文以一个小学生都能理解的例子来聊聊正确的`operator <`实现应该是怎样的。我们来比较两个数字的大小(为便于讨论,两个数都是两位的十进制数):
假如有:x = 36,y = 49;则 x < y 为真;
假如有:x = 36,y = 29;则 x < y 不为真;
假如有:x = 36,y = 39;则 x < y 为真;
假如有:x = 36,y = 32;则 x < y 不为真;
假如有:x = 36,y = 36;则 x < y 不为真;
我们是怎么正确比较出各种情况下 x 和 y 的大小的?实在是很简单:首先比较高位数(十位数),谁的高位数小,谁就小;谁的高位数大,谁就大;高位数相等的话,那就以相同的方式再比较低位数(个位数)。为什么要先比较高位数,因为高位数的“权”大啊!
类似的,比较SomeKey的时候,哪个字段先来,哪个字段就是主key,SomeKey中,a是主key,b是次key,即 a 对于SomeKey对象“大小”的贡献占主导地位,如果 a 能决出胜负来,b 就不用比了,只有 a 不能决出胜负时(相等),才继续比较 b,无论怎么比较,结果要是稳定的!
“实现3”和“实现4”要这样修改一下才行:
bool operator < (const SomeKey& left, const SomeKey& right)
{
if (left.a < right.a) // 主key
{
return true; // 主key小,就小
}
if (left.a > right.a) // 主key
{
return false; // 主key大,就大
}
return left.b < right.b; // 主key相等,再比较次key
}
说起来,本文其实不值一提!