赞
踩
虽然C++标准库提供了很多容器以供使用,但是实际上有时候其不能很好地满足一些较为特殊的需求,这时就需要自定义一些容器以满足实际的需要。在自定义一个容器之前,有必要先了解STL。因为STL中提供的容器的完备而优秀的设计,是容器代码的最好范本。
STL六大组件之间的交互关系:Container通过Allocator取得数据储存空间,Algorithm通过Iterator存取Container内容,Functor可以协助Algorithm完成不同的策略变化,Adapter可以修饰或套接Functor。
Java中提供了LinkedHashMap,能使得遍历的顺序和插入数据的顺序保持一致,但是C++标准库没有提供,考虑基于JDK的LinkedHashMa的实现思路,实现C++版本的LinkedHashMap。
std::list
)中存放实际的KV对;std::unordered_map
)中存放Key到双向链表中的相应位置迭代器的映射。C++11后提供了哈希表的实现,clang中标准库 std::unorder_map
的声明如下:
template <class _Key, class _Tp,
class _Hash = hash<_Key>,
class _Pred = equal_to<_Key>,
class _Alloc = allocator<pair<const _Key, _Tp> > >
class _LIBCPP_TEMPLATE_VIS unordered_map
{
...
}
可见实例化一个哈希表时,需要关注的几个模板参数是:
_key
;_Tp
;_Hash
;_Pred
;_Alloc
;通常来说提供前四个参数就可以满足绝大多数需求,那么 LinkedHashMap
的声明可以写成:
template <class KeyType, class MappedType,
class Hash = std::hash<KeyType>,
class Pred = std::equal_to<KeyType> >
class LinkedHashMap
{
...
}
首先因为每个容器都应提供自己的迭代器类型,所以需要重定义 LinedHashMap::iterator
,同时因为KV对是经常操作的一个数据结构,所以将 <Key, Value>
定义为一个类型:
public:
/**
* @brief 保存的entry的类型
*
**/
typedef std::pair<const KeyType, MappedType> EntryType;
/**
* @brief list中的非const迭代器类型
*
**/
typedef typename std::list<EntryType>::iterator iterator;
/**
* @brief list中的const迭代器类型
*
**/
typedef typename std::list<EntryType>::const_iterator const_iterator;
实际上,定义成员类型通常是必要的,因为有时需通过这种方式获取某个类的__内嵌依赖类型名(nested dependent type name),__其中内嵌指的是内嵌是指定义在类名的定义中的,而依赖指的是依赖于一个模板参数,例如上面的 EntryType
依赖于模板参数 KeyType
和 MappedType
。
如3.1所示,底层需要借助双向链表( std::list
)和哈希表( std::unordered_map
)来实现 LinkedHashMap
,所以这二者需要作为成员变量,另外为了方便处理,需要记录当前元素数量,具体如下:
private:
typedef std::unordered_map<KeyType, iterator, Hash, Pred> HashMap;
typedef typename HashMap::iterator _map_itr;
typedef typename HashMap::const_iterator _map_citr;
/**
* @brief entry的数量
*
**/
size_t _size;
/**
* @brief 实际存放entry的双向链表
*
**/
std::list<EntryType> _values;
/**
* @brief 保存key到entry的hashmap
*
**/
HashMap _map_key2entry;
这里定义的三个成员类型是方便内部实现的,容器使用者不可见。
除了构造函数和析构函数,一个容器最基础的功能是进行插入,遍历,查找和删除,下面分别进行说明:
/**
* @brief 在map中插入一个新entry
*
* @param [in] new_entry : 待添加的entry
**/
std::pair<iterator, bool> insert(const EntryType& new_entry) {
_map_itr pIt = _map_key2entry.find(new_entry.first);
if (pIt == _map_key2entry.end()) {
_values.push_back(new_entry);
_map_key2entry.insert(std::pair<KeyType, iterator>(new_entry.first, --_values.end()));
++_size;
}
else {
_values.erase(pIt->second);
_values.push_back(new_entry);
pIt->second = --_values.end();
}
return std::pair<iterator, bool>(--_values.end(), true);
}
/**
* @brief 在map中指定位置插入一个新entry
*
* @param [in] new_entry : 待添加的entry
* @note Since C++11
**/
iterator insert(const_iterator it, const EntryType& new_entry) {
_map_itr pIt = _map_key2entry.find(new_entry.first);
if(pIt != _map_key2entry.end()) {
_values.erase(pIt->second);
--_size;
}
iterator _it = _values.insert(it, new_entry);
_map_key2entry.insert(std::pair<KeyType, iterator>(new_entry.first, _it));
++_size;
return _it;
}
提供两种最常用的插入函数,分别是插入一个KV对,及在指定迭代器处插入一个KV对。
/**
* @brief 获取首元素的迭代器
*
* @param [in] null
**/
inline iterator begin() {
return _values.begin();
}
/**
* @brief 获取首元素的const迭代器
*
* @param [in] null
**/
inline const_iterator begin() const {
return _values.begin();
}
/**
* @brief 获取尾元素的迭代器
*
* @param [in] null
**/
inline iterator end() {
return _values.end();
}
/**
* @brief 获取尾元素的const迭代器
*
* @param [in] null
**/
inline const_iterator end() const {
return _values.end();
}
提供普通迭代器和常迭代器。实际上,如果编译器支持C++11,那么可以直接使用 for (auto entry : LinkedHashmap)
这种范围for语句来遍历。
/**
* @brief 在map中查找元素,如存在则返回其迭代器,否则返回end()
*
* @param [in] key : 指定的键值
**/
iterator find(const KeyType& key) {
if (_size > 0) {
_map_itr it = _map_key2entry.find(key);
if (it == _map_key2entry.end()) {
return _values.end();
}
iterator find_it = it->second;
return find_it;
}
return _values.end();
}
/**
* @brief find()的const版本
*
* @param [in] key : 指定的键值
**/
const_iterator find(const KeyType& key) const {
if (_size > 0) {
_map_citr it = _map_key2entry.find(key);
if (it == _map_key2entry.end()) {
return _values.end();
}
const_iterator find_it = it->second;
return find_it;
}
return _values.end();
}
提供普通查找函数,区别是返回的一个是普通迭代器,一个是常量迭代器。
/**
* @brief 移除位于pos处的entry
*
* @param [in] pos : 指定位置的迭代器
**/
void erase(iterator pos) {
assert(pos != _values.end());
assert(_size != 0);
EntryType v = *pos;
_values.erase(pos);
_map_key2entry.erase(v.first);
--_size;
}
/**
* @brief 移除指定键值的entry
*
* @param [in] key : 指定的键值
**/
void erase(const KeyType& key) {
iterator it = find(key);
if (it != _values.end()) {
erase(it);
}
}
/**
* @brief 清空所有entry
*
* @param [in] null
**/
void clear() {
_values.clear();
_map_key2entry.clear();
_size = 0;
}
提供两种删除函数,分别是指定迭代器位置删除和指定键值删除。同时提供一个清空函数,能够清空该map。
/**
* @brief 返回entries的数量
*
* @param [in] null
**/
inline size_t size() {
return _size;
}
/**
* @brief 获取该map是否为空
*
* @param [in] null
**/
inline bool empty() const {
return _values.empty();
}
提供两个函数,方便用户获取容器的当前元素数量和是否为空。
构造100万个std::pair<string,string>的KV对,对 std::unordered_map
和 LinkedHashMap
进行性能对比测试。
序号 | std::unordered_map 插入元素 (ms) | LinkedHashMap 插入元素 (ms) | 插入时间占比 |
---|---|---|---|
1 | 1112.26 | 1385.78 | 124.5913725 |
2 | 991.994 | 1294.81 | 130.5259911 |
3 | 1001.36 | 1333.16 | 133.1349365 |
4 | 983.257 | 1316.26 | 133.8673409 |
5 | 1083.24 | 1397.9 | 129.0480411 |
平均 | 1034.4222 | 1345.582 | 130.2335364 |
可以看到插入时间相比 std::unordered_map
大约有30%的性能损失。
序号 | std::unordered_map 遍历 (ms) | LinkedHashMap 遍历 (ms) | 遍历时间占比 |
---|---|---|---|
1 | 62.959 | 25.526 | 40.543846 |
2 | 63.014 | 24.585 | 39.01513949 |
3 | 62.48 | 25.136 | 40.23047375 |
4 | 63.413 | 25.07 | 39.53448031 |
5 | 67.074 | 26.71 | 39.82168948 |
平均 | 63.788 | 25.4054 | 39.82912581 |
顺序遍历的时间下降到std::unordered_map的40%左右,即速度提高了2.5倍左右;同时LinkedHashMap能够保证遍历的顺序同插入的顺序一致。
序号 | std::unordered_map 随机访问 (ms) | LinkedHashMap 随机访问 (ms) | 随机访问时间占比 |
---|---|---|---|
1 | 505.177 | 566.952 | 112.2283873 |
2 | 492.751 | 560.022 | 113.6521286 |
3 | 511.388 | 555.392 | 108.6048167 |
4 | 480.039 | 540.196 | 112.5316901 |
5 | 479.683 | 555.744 | 115.8565136 |
平均 | 493.8076 | 555.6612 | 112.5747072 |
可以看到,随机访问时间有12%左右的性能损失。
std::hash
和 std::equal_to
针对大多数类型都是可以正常工作的,其中 std::hash
针对所有C++标准类型都有其响应的特化版本,但是没有对 C 字符串的特化。 std::hash<const char*> 产生指针值(内存地址)的哈希,它不检验任何字符数组的内容。所以在使用字符指针这种比较特征的类型时,如果想要其按照字符指针所指向的字符串的内容来进行哈希和比较的话,需要定义自己的哈希functor Hash
和比较functor Pred
:
struct MyHash
{
std::size_t operator()(const char* s) const
{
std::hash<std::string> str_hasher;
std::size_t h1 = str_hasher(s);
return h1;
}
};
struct StrEqual {
bool operator()(const char *val1, const char *val2) const{
return strcmp(val1, val2) == 0;
}
};
LinkedHashmap<const char*, const char*, MyHash, strEqual>;
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。