C++中map
和unordered_map
提供的是一种键值对容器,在实际开发中会经常用到,它跟Python的字典很类似,所有的数据都是成对出现的,每一对中的第一个值称之为关键字(key),每个关键字只能在map中出现一次;第二个称之为该关键字的对应值(value)。
那么枚举类型(enum)可以作为键值对容器的key么?
问题引出
自定义一个枚举类型,并将其分别作为map和unordered_map的key:
#include <map>
#include <unordered_map>
#include <string>
typedef enum {
COLOR_INIT = 0,
COLOR_RED = 1,
COLOR_BLUE = 2,
COLOR_GREEN = 3,
COLOR_YELLOW = 4
} Color;
int main()
{
std::map<Color, std::string> color_map;
std::unordered_map<Color, std::string> color_unordered_map;
return 0;
}
C++11的编译环境下,编译过程中,就会出错:
In file included from main.cpp:1:
In file included from /root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/map:532:
In file included from /root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/__debug:15:
/root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/type_traits:1773:38: error: implicit instantiation of undefined template 'std::hash<Color>'
: public integral_constant<bool, __is_empty(_Tp)> {};
^
/root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/unordered_map:539:18: note: in instantiation of template class 'std::is_empty<std::hash<Color>>' requested here
bool = is_empty<_Hash>::value && !__libcpp_is_final<_Hash>::value>
^
/root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/unordered_map:1026:13: note: in instantiation of default argument for '__unordered_map_hasher<Color, std::__hash_value_type<Color, std::string>, std::hash<Color>, std::equal_to<Color>>' required here
typedef __unordered_map_hasher<key_type, __value_type, hasher, key_equal> __hasher;
^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
main.cpp:16:42: note: in instantiation of template class 'std::unordered_map<Color, std::string>' requested here
std::unordered_map<Color, std::string> color_unordered_map;
^
/root/emsdk/upstream/emscripten/cache/sysroot/include/c++/v1/__memory/shared_ptr.h:1552:50: note: template is declared here
template <class _Tp> struct _LIBCPP_TEMPLATE_VIS hash;
^
1 error generated.
从编译结果可以很明显地看出:enum可以直接作为map的key,却不能直接作为unordered_map的key。
不同编译器可能展示出来的出错log不同,但是里面都会有一个出现频率特别高的词——hash。也就是说,enum不能直接作为unordered_map的key肯定和hash有着很密切的关系。
原因分析
map和unordered_map的不同编译结果,主要体现在其内部构造的不同:
map的内部构造:
map的内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的。红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。
unordered_map的内部构造:
unordered_map的内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的。
从内部构造上可以看出:
unordered_map | map | |
---|---|---|
查找 | 快,Average O(1),worst O(n) | 恒定log(n) |
插入 | 快,Average O(1),worst O(n) | log(n)+平衡二叉树调整时间 |
删除 | 快,Average O(1),worst O(n) | log(n)+平衡二叉树调整时间 |
适用于 | 查找频率高 | 要求有序 |
因此,可以看出对于unordered_map使用哈希表,因此在自定义的数据结构作为key的时候,需要指定其hash计算方式。不然就会出问题。
问题解决
定义模板对象来计算枚举的哈希值,同时作为unordered_map的第三个参数传入:
#include <unordered_map>
#include <string>
#include <iostream>
typedef enum {
COLOR_INIT = 0,
COLOR_RED = 1,
COLOR_BLUE = 2,
COLOR_GREEN = 3,
COLOR_YELLOW = 4
} Color;
struct EnumClassHash
{
template <typename T>
std::size_t operator()(T t) const
{
return static_cast<std::size_t>(t);
}
};
int main()
{
std::unordered_map<Color, std::string, EnumClassHash> color_unordered_map;
return 0;
}
需要注意的是:无需提供std::hash<>的特殊化,模板参数推导即可完成。
当然,可以使用单词using并根据key是否为枚举类型,从而选择使用EnumClassHash还是std::hash,从而定义一个新的MyUnorderedMap:
template <typename T>
using HashType = typename std::conditional<
std::is_enum<T>::value, EnumClassHash, std::hash<T> >::type;
template <typename Key, typename T>
using MyUnorderedMap = std::unordered_map<Key, T, HashType<Key> >;
这里,通过std::conditional进行条件判断,如果T是enum枚举类型,就是EnumClassHash,否则为默认的std::hash。
这样的话,直接使用MyUnorderedMap,不用担心key是不是枚举类型了:
MyUnorderedMap<Color, std::string> color_unordered_map;
最最最重要的是:
这被认为是标准中的缺陷,并且已在C++14中得到修复:C++ Standard Library Defect Reports and Accepted Issues (Revision R124)。
如果你是C++14及其以上的版本,那么就不会有这个问题了。