如何使用用户定义类型作为std::map的键?

90
我想知道为什么我不能使用STL映射(maps)与用户定义的类一起使用。当我编译下面的代码时,会出现以下难懂的错误信息。这是什么意思?另外,为什么只有在使用用户定义的类型时才会发生这种情况?(当它们被用作键时,原始类型是可以使用的。)

C:\MinGW\bin..\lib\gcc\mingw32\3.4.5........\include\c++\3.4.5\bits\stl_function.h||在成员函数`bool std::less<_Tp>::operator()(const _Tp&, const _Tp&) const [with _Tp = Class1]'中:|

C:\MinGW\bin..\lib\gcc\mingw32\3.4.5........\include\c++\3.4.5\bits\stl_map.h|338|来自 '_Tp& std::map<_Key, _Tp, _Compare, _Alloc>::operator[](const _Key&) [with _Key = Class1, _Tp = int, _Compare = std::less, _Alloc = std::allocator >]' 的实例化代码|

C:\Users\Admin\Documents\dev\sandbox\sandbox\sandbox.cpp|24|在此实例化|

C:\MinGW\bin..\lib\gcc\mingw32\3.4.5........\include\c++\3.4.5\bits\stl_function.h|227|错误:'__x < __y'中没有匹配的运算符| ||=== Build finished: 1 errors, 0 warnings ===|

#include <iostream>
#include <map>

using namespace std;

class Class1
{
public:
    Class1(int id);

private:
    int id;
};

Class1::Class1(int id): id(id)
{}

int main()
{
    Class1 c1(1);

    map< Class1 , int> c2int;
    c2int[c1] = 12;

    return 0;
}

可能是C++使用自定义类类型作为键的unordered_map的重复问题。 - Ciro Santilli OurBigBook.com
8个回答

179

实际上,您不必为类定义operator<。相反,您也可以创建一个比较函数对象类,并使用它来特化std::map。扩展您的示例:

struct Class1Compare
{
   bool operator() (const Class1& lhs, const Class1& rhs) const
   {
       return lhs.id < rhs.id;
   }
};

std::map<Class1, int, Class1Compare> c2int;

std::map的第三个模板参数的默认值恰好是std::less,它将委托给您的类定义的operator<(如果没有则失败)。但有时您希望对象可用作地图键,但实际上并没有任何有意义的比较语义,因此您不希望仅为此提供类的operator<而使人们感到困惑。如果是这种情况,可以使用上述技巧。

另一种实现相同效果的方法是特化std::less

namespace std
{
    template<> struct less<Class1>
    {
       bool operator() (const Class1& lhs, const Class1& rhs) const
       {
           return lhs.id < rhs.id;
       }
    };
}

这样做的好处是,默认情况下,std::map 会自动选择它,并且您不会把 operator< 接口暴露给客户端代码。


9
我建议在这两个函数中添加const关键字。 - Diomidis Spinellis
也许与结构体少使用 friend 是值得的,否则我认为这将是一种妥协的封装。 - SkyWalker
模板化结构体应该以分号结束,否则会出现编译错误。不幸的是,由于更改字符数量较少,我无法通过编辑来修复这个问题。 - Ident
但是为什么你要把结构体放在less info std里面呢? - Vladimir Tsyshnatiy
它已经在std中了。这只是那个的一个特化。 - Pavel Minaev

35

默认情况下,std::map(以及std::set)使用operator<来确定排序。因此,您需要在您的类上定义operator<

如果两个对象满足条件!(a < b) && !(b < a),则认为它们是等效的

如果出于某种原因,您想要使用不同的比较器,则可以更改map的第三个模板参数,例如使用std::greater


3
实际上,您可以将比较器更改为任何双参数函数。 - xtofl

22

你需要为Class1定义operator <

当用户自定义类作为键时,Map需要使用operator <比较值,因此你需要提供相同的操作符。

class Class1
{
public:
    Class1(int id);

    bool operator <(const Class1& rhs) const
    {
        return id < rhs.id;
    }
private:
    int id;
};

1
它不需要 operator<;它只是默认为它。请参考 GMan 或 Pavel 的答案。 - xtofl

5
class key
{
    int m_value;
public:
    bool operator<(const key& src)const
    {
        return (this->m_value < src.m_value);
    }

};
int main()
{
    key key1;
    key key2;
    map<key,int> mymap;
    mymap.insert(pair<key,int>(key1,100));
    mymap.insert(pair<key,int>(key2,200));
    map<key,int>::iterator iter=mymap.begin();
    for(;iter!=mymap.end();++iter)
    {
        cout<<iter->second<<endl;
    }


}

6
欢迎来到StackOverflow!请为您的答案添加一些解释。 - Aurasphere

4

如果你只加入下面这行代码,那么你的示例就能在C++20中运行

auto operator<=>(Class1 const &) const = default;

到你的班级。


4
我想进一步扩展 Pavel Minaev答案,在阅读我的答案之前,您应该先阅读他的答案。如果要比较的成员(例如问题代码中的id)是私有的,则 Pavel 提出的两种解决方案都无法编译。在这种情况下,对于我来说,VS2013会抛出以下错误:

error C2248: 'Class1::id' : cannot access private member declared in class 'Class1'

正如 SkyWalker评论中提到的,使用friend声明可以帮助解决问题。如果您想了解正确的语法,请参阅以下内容:
class Class1
{
public:
    Class1(int id) : id(id) {}

private:
    int id;
    friend struct Class1Compare;      // Use this for Pavel's first solution.
    friend struct std::less<Class1>;  // Use this for Pavel's second solution.
};

在 Ideone 上的代码

但是,如果你有一个访问私有成员的函数,例如对于 idgetId(),如下:

class Class1
{
public:
    Class1(int id) : id(id) {}
    int getId() const { return id; }

private:
    int id;
};

然后您可以使用它来代替friend声明(即比较lhs.getId() < rhs.getId())。 自C++11以来,您还可以使用lambda表达式来替代定义比较器函数对象类的方式,用于Pavel的第一种解决方案。 将所有内容放在一起,代码可以编写如下:

auto comp = [](const Class1& lhs, const Class1& rhs){ return lhs.getId() < rhs.getId(); };
std::map<Class1, int, decltype(comp)> c2int(comp);

在 Ideone 上的代码


你能告诉我为什么成员变量不可访问,因为operator<重载发生在同一个类内吗?所以即使是私有成员变量,在类内也应该是可访问的。 - m_alpha
1
@m_alpha:Pavel和我提供的解决方案没有为Class1定义operator<。比较器是在Class1之外定义的。因此,它们无法访问Class1的私有成员。 - honk

3

键必须是可比较的,但你没有为你的自定义类定义一个合适的operator<


1

正确的解决方案是为您的类/结构体专门化std::less

• 基本上,cpp中的映射是作为二叉搜索树实现的。

  1. BST将节点的元素进行比较以确定树的组织。
  2. 元素比父节点的元素小的节点放置在父节点的左侧,元素比父节点的元素大的节点放置在右侧。 即

对于每个节点,node.left.key < node.key < node.right.key

BST中的每个节点都包含元素,在地图的情况下,它的KEY和值,键应该按顺序排列。 有关映射实现的更多信息:Map数据类型

在cpp地图的情况下,键是节点的元素,值不参与树的组织,只是一个补充数据。

这意味着键应与 std::lessoperator< 兼容,以便它们可以被组织。请查看 map parameters
否则,如果您使用用户定义的数据类型作为键,则需要为该数据类型提供有意义的比较语义。
解决方案:专门化 std::less
映射模板中的第三个参数是可选的,它是 std::less,它将委托给 operator<
因此,为您的用户定义的数据类型创建一个新的 std::less。现在,默认情况下,std::map 将选择此新的 std::less
namespace std
{
    template<> struct  less<MyClass>
    {
        bool operator() (const MyClass& lhs, const MyClass& rhs) const
        {
            return lhs.anyMemen < rhs.age;
        }
    };

}

注意:如果您想将用户定义的数据类型用作cpp映射的键,则需要为每个数据类型创建专门的std :: less不好的解决方案:为您的用户定义的数据类型重载operator<。这种解决方案也可以工作,但是它非常糟糕,因为运算符<将被普遍重载为您的数据类型/类。这在客户端场景中是不可取的。
请查看答案Pavel Minaev的答案

“overloaded universally” 是什么意思? - Tyler Shellberg
重载 operator < 在实例自然可比较时更有意义和用处。例如:struct Level { int level1; int level2; }; - Alexander Chen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接