关联容器:永远让比较函数对相等的值返回false

本文探讨了在C++中使用set与multiset时,因比较函数不当而导致的未定义行为问题。详细分析了使用less_equal作为比较类型时如何破坏set的特性,并提供了正确的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、问题引入

让我向你展示一些比较酷的东西。

建立一个set,比较类型用less_equal,然后插入一个10:

// s以“<=”排序

set<int, less_equal<int> > s;   
s.insert(10);   

现在尝试再插入一次10:

s.insert(10)  


对于这个insert的调用,set必须先要判断出10是否已经位于其中了。 我们知道它是,但set可是木头木脑的,它必须执行检查。为了便于弄明白发生了什么,我们将一开始已经在set中的10称为10A,而正试图插入的那个10叫10B。 


set遍历它的内部数据结构以查找哪儿适合插入10B。最终,它总要检查10B是否与10A相同。关联容器对“相同”的定义是等价,因此set测试10B是否等价于10A。当执行这个测试时,它自然是使用set的比较函数。在这一例子里,是operator<=,因为我们指定set的比较函数为less_equal,而less_equal意思就是operator<=。

于是,set将计算这个表达式是否为真:

// 测试10A和10B是否等价
!(10A <= 10B) && !(10B <= 10A)


哦,10A和10B都是10,因此,10A <= 10B 肯定为真。同样清楚的是,10B <= 10A。于是上述的表达式简化为
!(true) && !(true)
再简化就是
false && false
结果当然是false。 也就是说,set得出的结论是10A与10B不等价,因此不一样,于是它将10B插入容器中10A的旁边。


在技术上而言,这个做法导致未定义的行为,但是通常的结果是set以拥有了两个为10的值的拷贝而告终,也就是说它不再是一个set了。通过使用less_equal作为我们的比较类型,我们破坏了容器!此外,任何对相等的值返回true的比较函数都会做同样的事情。是不是很酷?


二、解决思路     

举个例子,前一篇博客描述了该如何写一个比较函数以使得容纳string*指针的容器根据string的值排序,而不是对指针的值排序。那个比较函数是按升序排序的,但我们现在假设你需要string*指针的容器的降序排序的比较函数。自然是抓现成的代码来修改了。如果你不细心,可能会这么干:


// 当心,这代码是有瑕疵的!只是相反了旧的测试,这是不对的!

struct StringPtrGreater : public binary_function<const string*, const string*, bool> {
        bool operator()(const string *ps1, const string *ps2) const{
                return !(*ps1 < *ps2);                  
        }                                              
};


这里的想法是通过将比较函数内部结果取反来达到反序的结果。很不幸,取反“<”不会给你(你所期望的)“>”,它给你的是“>=”。而你现在知道,因为它将对相等的值返true,对关联容器来说,它是一个无效的比较函数


你真正需要的比较类型是这个:

 // 对关联容器来说,这是有效的比较类型
struct StringPtrGreater : public binary_function<const string*, const string*,bool> {
        bool operator()(const string *ps1, const string *ps2) const{
                return *ps2 < *ps1;                     

// 返回*ps2是大于*ps1(也就是交换操作数的顺序)
        }                                               
};

要避免掉入这个陷阱,你所要记住的就是比较函数的返回值表明的是在此函数定义的排序方式下,一个值是否大于另一个。相等的值绝不该一个大于另一个,所以比较函数总应该对相等的值返回false。


三、问题拓展:multiset和multimap  

让我们返回头去看最初的例子,但这次使用的是一个mulitset:

// s仍然以“<=”排序
multiset<int, less_equal<int> > s;

// 插入10A                      
s.insert(10);

// 插入10B                                   
s.insert(10);                                   


现在,s里有两个10的拷贝,因此我们期望如果我们在它上面做一个equal_range,我们将会得到一对指出包含这两个拷贝的范围的迭代器。但那是不可能的。equal_range,虽然叫这个名字,但不是指示出相等的值的范围,而是等价的值的范围。在这个例子中,s的比较函数说10A和10B是不等价的,所以不可能让它们同时出现在equal_range所指示的范围内。


你明白了吗?除非你的比较函数总是为相等的值返回false,你将会打破所有的标准关联型容器,不管它们是否允许存储复本。                                               

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值