不能对 std::set<std::string, std::less 使用用户提供的比较函数<>>

Cannot use user-provided comparison function for std::set<std::string, std::less<>>

本文关键字:std lt gt 比较 函数 用户 set string 不能 less      更新时间:2023-10-16

std::less的帮助下,我偶然发现了使用透明比较器的std::set的非常奇怪的编译错误。考虑一下这个简单的程序:

using Key = std::string;
bool operator<(const Key&, int) { return true; }
bool operator<(int, const Key&) { return true; }
int main()
{
std::set<Key, std::less<>> s;
int x;
auto it = s.find(x);
}

它给了我编译错误:

error: no matching function for call to object of type 'const std::less<void>'
if (__j != end() && _M_impl._M_key_compare(__k, _S_key(__j._M_node)))
^~~~~~~~~~~~~~~~~~~~~~

如果我使用自己的类而不是std::string作为密钥,它会很好地工作:

struct My {};
bool operator<(const My&, const My&) { return true; }
using Key = My;

为什么它不适用于std::string

请在此处查看演示:https://gcc.godbolt.org/z/MY-Y2s

UPD

我真正想做的是声明std::unique_ptr<T>T*之间的比较运算符。但我认为std::string会更清楚。

依赖于参数的查找是一件有趣的老事情,不是吗?

在名称空间std中已经存在与std::string相关的operator<,这是在寻找适合您的参数的<时发现的。也许与直觉相反(但并非没有充分的理由),之后不会尝试进一步查找!不搜索其他命名空间。即使只有您的重载才真正匹配这两个参数。您的全局operator<在这种情况下被有效地隐藏起来,如以下可怕的示例所示:

namespace N
{
struct Foo {};
bool operator<(Foo, Foo) { return false; }
}
bool operator<(N::Foo, int) { return false; }
namespace N
{
template <typename T1, typename T2>
bool less(T1 lhs, T2 rhs)
{
return lhs < rhs;
}
}
int main()
{
N::Foo f;
N::less(f, 3);
}
/*
main.cpp: In instantiation of 'bool N::less(T1, T2) [with T1 = N::Foo; T2 = int]':
main.cpp:22:17:   required from here
main.cpp:15:20: error: no match for 'operator<' (operand types are 'N::Foo' and 'int')
return lhs < rhs;
~~~~^~~~~
*/

(现场演示)

现在,您不能向命名空间std添加内容,但这很好,因为如果您无论如何都不重载与其他人的类型相关的运算符,它会更好。当其他库做同样的事情时,这是隐藏ODR错误的最快方法。

类似地,创建一个名为Key的东西,实际上是,只是伪装成std::string,这会导致意外冲突和意外行为。

总的来说,我强烈建议您的Key至少是std::string的"强别名";也就是说,它自己的类型而不是简单的别名。正如您所发现的,这也解决了您的问题,因为现在operator<和操作数类型在同一个命名空间中。

更一般地说,如果您不是真的在这里使用别名,但确实想对标准类型进行操作,那么您将重新编写一个命名的自定义比较器,它可以很好地隔离新逻辑,而且使用起来也很简单。当然,不利的一面是你每次都必须"选择加入",但我认为这总体上是值得的。