无法将枚举类用作unordered_map的键

93

我有一个包含枚举类的类。

class Shader {
public:
    enum class Type {
        Vertex   = GL_VERTEX_SHADER,
        Geometry = GL_GEOMETRY_SHADER,
        Fragment = GL_FRAGMENT_SHADER
    };
    //...

那么,当我在另一个类中实现以下代码时...

std::unordered_map<Shader::Type, Shader> shaders;

...我遇到了编译错误。

...usr/lib/c++/v1/type_traits:770:38: 
Implicit instantiation of undefined template 'std::__1::hash<Shader::Type>'

这里是什么导致了错误?


6
你没有为枚举类型专门指定std::hash - Kerrek SB
8个回答

131

我使用一个函数对象来计算enum class的哈希值:

struct EnumClassHash
{
    template <typename T>
    std::size_t operator()(T t) const
    {
        return static_cast<std::size_t>(t);
    }
};

现在,您可以将其用作std::unordered_map的第三个模板参数:
enum class MyEnum {};

std::unordered_map<MyEnum, int, EnumClassHash> myMap;

因此,您无需提供std::hash的专业化,模板参数推断会完成这项工作。此外,您可以使用单词using并创建自己的unordered_map,该映射使用std::hashEnumClassHash,具体取决于Key类型:

template <typename Key>
using HashType = typename std::conditional<std::is_enum<Key>::value, EnumClassHash, std::hash<Key>>::type;

template <typename Key, typename T>
using MyUnorderedMap = std::unordered_map<Key, T, HashType<Key>>;

现在您可以使用MyUnorderedMapenum class或其他类型:
MyUnorderedMap<int, int> myMap2;
MyUnorderedMap<MyEnum, int> myMap3;

理论上,HashType 可以使用 std::underlying_type,那么就不需要使用 EnumClassHash。代码如下,但我还没有尝试过
template <typename Key>
using HashType = typename std::conditional<std::is_enum<Key>::value, std::hash<std::underlying_type<Key>::type>, std::hash<Key>>::type;

如果使用std::underlying_type可行的话,可能会成为标准的一个非常好的建议。


1
也许最简单的方法是,首先让枚举键正常工作,这样会更简单吗? :-S - Jonny
从理论上讲,underlying_type 不起作用。你已经证明了:必须有一个 hash() 函数,它接受一个 MyEnumClass 参数。因此,当在 underlying_type 上进行哈希时,会调用一个期望一个 int(或 : yourEnumClassType)的函数。仅仅尝试使用 underlying_type 将显示它给出完全相同的错误:无法将 MyEnumClass 转换为 int。如果仅仅传递 underlying_type 就可以工作,那么直接在第一次传递 MyEnumClass 也是可以的。无论如何,正如 David S 所示,WG 现在已经修复了这个问题。如果 GCC 能够发布他们的补丁就好了... - underscore_d
如果枚举类是另一个类的受保护“成员”,那么这种方法对我不起作用。我需要将枚举定义移到类之外,直接放在命名空间定义中。 - Martin Pecka
1
由于某些原因,我在使用枚举类作为无序映射中的键时没有任何问题。我使用clang,也许支持取决于编译器?编辑:正如另一个答案所指出的那样,这已经成为C++14标准的一部分。 - johnbakers
1
被接受的答案应该被更改,指向那个起始于指出这种行为在标准中被认为是一个缺陷,并已在现代编译器中得到修复的答案。 - mallwright
显示剩余2条评论

62

1
我很惊讶在Visual Studio 2013中使用enum class作为std::unordered_set的键可以编译通过。 - Richard Dally
3
很可能是有意修复的。我认为在Visual Studio 2012中无法正常工作,所以他们很可能在2013年修复了它作为该缺陷的一部分。实际上,微软的C ++标准库维护人员STL提供了解决此缺陷的措辞。 - David Stone
1
@ypnos:我更喜欢Visual Studio的方法,即让所有人都使用最新版本的语言,因为C++现在就是C++14。 - David Stone
2
事实上,当前的Visual Studio不支持C++14甚至C++11。它们都包括C99,而Visual Studio不支持C99。我并不是说VS不应该默认使用最新的语言版本。我是说,在存在一些被所有竞争编译器支持的标准时,它不应该引入自己的事实标准。VS 2013发布于C++14最终确定的整整一年之前。然而,它没有完全支持C++11,而是包含了C++14功能的一个子集。 - ypnos
2
相信我,我不只是给你扔了一个链接——首先,该页面确实描述了C++14,只需向上滚动即可。其次,“最小化垃圾回收支持”符合标准。如果您阅读规范(在那里有链接!):“不支持垃圾回收并将所有库调用实现为无操作的实现是符合规范的。”这就是GCC所做的。我不明白您编写代码的平台或您喜欢的编译器如何增加讨论的内容,这个问题是关于标准符合性而不是关于个人感知的编译器质量。 - ypnos
显示剩余9条评论

27

一个非常简单的解决方案是提供一个像这样的哈希函数对象:

std::unordered_map<Shader::Type, Shader, std::hash<int> > shaders;

这就是枚举键的全部内容,不需要提供 std::hash 的专门化实现。


30
这适用于旧式的枚举类型,但不适用于 OP 正在使用的新型 "枚举类"。 - BrandonLWhite
6
这个回答明显没有回答问题,为什么它的赞数能达到 +8? - underscore_d
根据下面弗拉基米尔的回答,也许denim在VS2012或其他某些编译器中测试了这个,使其成为可能。 - underscore_d

7
将以下内容添加到定义MyEnumClass的标题中:
namespace std {
  template <> struct hash<MyEnumClass> {
    size_t operator() (const MyEnumClass &t) const { return size_t(t); }
  };
}

1
你不应该在签名中添加 const noexcept 吗? - einpoklum
不幸的是,扩展std是未定义行为。 - Victor Polevoy
5
@VictorPolevoy:幸运的是,对于这种特殊情况,扩展std是有定义行为的。http://en.cppreference.com/w/cpp/language/extending_std - galinette
我认为 LLVM 8.1 在这方面存在问题,但在其他编译器上运行良好。 - Moshe Rabaev

5
正如KerrekSB所指出的那样,如果您想使用std :: unordered_map,您需要提供std :: hash的专门化版本,类似于以下内容:
namespace std
{
    template<>
    struct hash< ::Shader::Type >
    {
        typedef ::Shader::Type argument_type;
        typedef std::underlying_type< argument_type >::type underlying_type;
        typedef std::hash< underlying_type >::result_type result_type;
        result_type operator()( const argument_type& arg ) const
        {
            std::hash< underlying_type > hasher;
            return hasher( static_cast< underlying_type >( arg ) );
        }
    };
}

4
当您使用 std::unordered_map 时,您需要一个哈希函数。对于内置或 STL 类型,有默认值可用,但对于用户定义的类型则没有。如果您只需要一个映射,为什么不尝试使用 std::map 呢?

34
std::unordered_map 在几乎所有情况下都具有卓越的性能,应该被视为比 std::map 更为默认的选择。 - David Stone

0
当我想要从枚举类型到字符串获取unordered_map时,我遇到了类似的问题。
你很可能不需要unordered_map。因为你使用枚举类作为键,我假设你不需要像插入或删除这样的操作:简单的查找就足够了。
话虽如此,为什么不只是使用一个函数呢?
// assume shader_vertex, shader_geometry, shader_fragment are initialized
Shader* toShader(Shader::Type value){
  switch(value){
    case Shader::Type::Vertex:
      return &shader_vertex;
    case Shader::Type::Geometry:
      return &shader_geometry;
    case Shader::Type::Fragment:
      return &shader_fragment;
    default:
      //some error handling..
      return nullptr;
}

与其他解决方案不同,这种方法不依赖于编译器。

感谢这个项目链接


-1

尝试一下

std::unordered_map<Shader::Type, Shader, std::hash<std::underlying_type<Shader::Type>::type>> shaders;

无法工作。std::hash()将期望一个underlying_type实例作为参数,但实际上会得到MyEnumClass。这正是当您尝试使用旧的普通enum解决方案指定std::hash<int>时发生的情况。在建议之前,您是否尝试过这个方法? - underscore_d
当然,我做了。在VS 2012中编译没有问题。确切地说是这样的 "namespace ObjectDefines { enum ObjectType { ObjectHigh, .... } } std::unordered_map<ObjectDefines::ObjectType, ObjectData*, std::hash<std::underlying_typeObjectDefines::ObjectType::type>> m_mapEntry;" - Vladimir Shutow
这个问题是关于enum class,而不是C风格的未作用域的enum。你会发现我的评论对于主题enum class是正确的。 - underscore_d
我看到了区别。但它仍然可以编译通过: class ObjectDefines { public: enum class ObjectType { ObjectHigh, ObjectLow }; }; std::unordered_map<ObjectDefines::ObjectType, ObjectDefines*, std::hash<std::underlying_typeObjectDefines::ObjectType::type>> m_mapEntry; - Vladimir Shutow
1
在gcc 4.7.3中也可以编译(在此处进行了检查http://melpon.org/wandbox/permlink/k2FopvmxQeQczKtE)。 - Vladimir Shutow
显示剩余2条评论

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