std :: unordered_map :: find使用与Key类型不同的类型?

时间:2016-01-04 17:42:12

标签: c++ c++11 unordered-map

我有unordered_map使用字符串类型作为键:

std::unordered_map<string, value> map;

std::hash提供string专业化,以及a 合适的operator==

现在我还有一个“字符串视图”类,它是一个指向现有字符串的弱指针,避免了堆分配:

class string_view {
    string *data;
    size_t begin, len;
    // ...
};  

现在,我希望能够使用string_view对象检查地图中是否存在密钥。不幸的是,std::unordered_map::find采用Key参数,而不是通用的T参数。

(当然,我可以“推广”一个到string,但这会导致我想避免的分配。)

我喜欢的是

之类的东西
template<class Key, class Value>
class unordered_map
{
    template<class T> iterator find(const T &t);
};

需要operator==(T, Key)std::hash<T>()进行适当定义,并将迭代器返回到匹配值。

有解决方法吗?

9 个答案:

答案 0 :(得分:11)

如上所述,C ++ 14不为std::unordered_map提供异构查找(与std::map不同)。您可以使用Boost.MultiIndex来定义std::unordered_map的非常接近的替代,它允许您在不分配临时string_view的情况下查找std::string

<强> Live Coliru Demo

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/member.hpp>
#include <string>

using namespace boost::multi_index;

struct string_view
{
  std::string *data;
  std::size_t begin,len;
};

template<typename T,typename Q>
struct mutable_pair
{
  T         first;
  mutable Q second;
};

struct string_view_hash
{
  std::size_t operator()(const string_view& v)const
  {
     return boost::hash_range(
       v.data->begin()+v.begin,v.data->begin()+v.begin+v.len);
  }
  std::size_t operator()(const std::string& s)const
  {
     return boost::hash_range(s.begin(),s.end());
  }
};

struct string_view_equal_to
{
  std::size_t operator()(const std::string& s1,const std::string& s2)const
  {
     return s1==s2;
  }
  std::size_t operator()(const std::string& s1,const string_view& v2)const
  {
     return s1.size()==v2.len&&
            std::equal(
              s1.begin(),s1.end(),
              v2.data->begin()+v2.begin);
  }
  std::size_t operator()(const string_view& v1,const std::string& s2)const
  {
     return v1.len==s2.size()&&
            std::equal(
              v1.data->begin()+v1.begin,v1.data->begin()+v1.begin+v1.len,
              s2.begin());
  }
};

template<typename Q>
using unordered_string_map=multi_index_container<
  mutable_pair<std::string,Q>,
  indexed_by<
    hashed_unique<
      member<
        mutable_pair<std::string,Q>,
        std::string,
        &mutable_pair<std::string,Q>::first
      >,
      string_view_hash,
      string_view_equal_to
    >
  >
>;

#include <iostream>

int main()
{
  unordered_string_map<int> m={{"hello",0},{"boost",1},{"bye",2}};

  std::string str="helloboost";
  auto it=m.find(string_view{&str,5,5});
  std::cout<<it->first<<","<<it->second<<"\n";
}

<强>输出

boost,1

答案 1 :(得分:5)

就像最近C ++ 14一样,即使基本的map在比较中得到is_transparent类型的模板化查找也是如此。最有可能的是,散列容器的正确实现并不是很明显。

据我所知,你的两个选择是:

答案 2 :(得分:3)

P0919R2 Heterogeneous lookup for unordered containers已合并到C ++ 2a的工作草案中!

摘要似乎很合适我最初的问题:-)

  

摘要

     

此建议为C ++标准库中的无序关联容器添加了异构查找支持。结果,当提供不同(但兼容)类型作为成员函数的键时,不需要创建临时键对象。这也使无序和常规的关联容器接口和功能彼此之间更加兼容。

     

通过本文提出的更改,以下代码将可以正常工作,而不会带来任何其他性能上的损失:

template<typename Key, typename Value>
using h_str_umap = std::unordered_map<Key, Value, string_hash>;
h_str_umap<std::string, int> map = /* ... */;
map.find("This does not create a temporary std::string object :-)"sv);

答案 3 :(得分:2)

我面临同样的问题。

我们需要两个结构:

struct string_equal {
    using is_transparent = std::true_type ;

    bool operator()(std::string_view l, std::string_view r) const noexcept
    {
        return l == r;
    }
};


struct string_hash {
    using is_transparent = std::true_type ;

    auto operator()(std::string_view str) const noexcept {
        return std::hash<std::string_view>()(str);
    }
};

对于unordered_map:

template <typename Value>
using string_unorderd_map = std::unordered_map<std::string, Value, string_hash, string_equal>;

对于无序集:

using string_unorderd_set = std::unordered_set<std::string, string_hash, string_equal>;

现在可以使用string_view。

答案 4 :(得分:0)

此解决方案存在缺陷,可能会或可能不会使您的上下文不可行。

你可以创建一个包装类:

struct str_wrapper {
  const char* start, end;
};

并更改地图以使用str_wrapper作为其键。你必须为str_wrapper添加2个构造函数,一个用于std :: string,另一个用于string_view。主要决定是使这些构造函数执行深层还是浅层复制。

例如,如果仅将std :: string用于插入而str_view仅用于查找,则将std :: string构造函数设置为深层,并将str_view设置为浅层(如果使用的话,可以在编译时强制执行此操作unordered_map周围的自定义包装器)。如果您想避免深拷贝上的内存泄漏,则需要额外的字段来支持正确的破坏。

如果您的用法更加多样化(查找std :: string或通过str_view插入),则会有缺点,这可能会使方法过于令人反感,从而无法实现。这取决于您的预期用途。

答案 5 :(得分:0)

另一种选择是通过使用多个容器来拆分查找和数据管理:

std::unordered_map<string_view, value> map;
std::vector<unique_ptr<const char[]>> mapKeyStore;

使用string_view完成查找,无需分配。 每当插入新密钥时,我们都需要先添加实际的字符串分配:

mapKeyStore.push_back(conv(str)); // str can be string_view, char*, string... as long as it converts to unique_ptr<const char[]> or whatever type
map.emplace(mapKeyStore.back().get(), value)

std::string中使用mapKeyStore将更加直观。但是,使用std::string不能保证不变的字符串存储空间(例如,如果向量调整了大小)。使用unique_ptr可以强制执行。但是,我们需要一些特殊的转换/分配例程,在该示例中称为conv。如果您有一个自定义的字符串容器,可以保证移动中的数据一致性(并强制矢量使用移动),则可以在此处使用它。

缺点

上述方法的缺点是,如果天真地完成删除操作,则是不平凡且昂贵的。如果仅创建一次地图或仅对其进行扩展,则这不是问题,并且上述模式效果很好。

运行示例

下面的示例包括一个键的天真删除。

#include <vector>
#include <unordered_map>
#include <string>
#include <string_view>
#include <iostream>
#include <memory>
#include <algorithm>

using namespace std;
using PayLoad = int;

unique_ptr<const char[]> conv(string_view str) {
    unique_ptr<char[]> p (new char [str.size()+1]);
    memcpy(p.get(), str.data(), str.size()+1);
    return move(p);
}

int main() {
    unordered_map<string_view, PayLoad> map;
    vector<unique_ptr<const char[]>> mapKeyStore;
    // Add multiple values
    mapKeyStore.push_back(conv("a"));
    map.emplace(mapKeyStore.back().get(), 3);
    mapKeyStore.push_back(conv("b"));
    map.emplace(mapKeyStore.back().get(), 1);
    mapKeyStore.push_back(conv("c"));
    map.emplace(mapKeyStore.back().get(), 4);
    // Search all keys
    cout << map.find("a")->second;
    cout << map.find("b")->second;
    cout << map.find("c")->second;
    // Delete the "a" key
    map.erase("a");
    mapKeyStore.erase(remove_if(mapKeyStore.begin(), mapKeyStore.end(),
        [](const auto& a){ return strcmp(a.get(), "a") == 0; }),
        mapKeyStore.end());
    // Test if verything is OK.
    cout << '\n';
    for(auto it : map)
        cout << it.first << ": " << it.second << "\n";

    return 0;
}

当然,可以将这两个容器放入包装器中,该包装器自己处理插入和删除操作。

答案 6 :(得分:0)

我将只介绍我在github上找到的一个变体,它涉及定义一个包装std的新地图类。
重新定义一些密钥API来拦截我们想要的适配器,并使用静态字符串来复制密钥。
不一定是一个好的解决方案,但很有趣的是知道它对于认为足够的人而言是存在的。

原件:
https://gist.github.com/facontidavide/95f20c28df8ec91729f9d8ab01e7d2df

代码要点:

template <typename Value>
class StringMap: public std::unordered_map<std::string, Value>
{
public:
    typename std::unordered_map<string,Value>::iterator find(const nonstd::string_view& v )
    {
        tmp_.reserve(  v.size() );
        tmp_.assign( v.data(), v.size() );
        return std::unordered_map<string, Value>::find(tmp_);
    }

    typename std::unordered_map<std::string,Value>::iterator find(const std::string& v )
    {
        return std::unordered_map<std::string, Value>::find(v);
    }

    typename std::unordered_map<std::string,Value>::iterator find(const char* v )
    {
        tmp_.assign(v);
        return std::unordered_map<std::string, Value>::find(v);
    }

private:
    thread_local static std::string tmp_;
};

学分:
Davide Faconti

答案 7 :(得分:-1)

很抱歉回答这个非常老的问题,但它仍然出现在搜索引擎结果中... 在这种情况下,您的unordered_map使用字符串类型作为其键,则find方法正在查找对不会生成分配的字符串的引用。您的string_view类存储指向字符串的指针。因此,您的string_view类可以将指针取消引用到映射所需类型的ref中,而不会引起分配。该方法看起来像这样...

string &string_view::getRef() const
{
    return *_ptr;
}

并使用string_view和地图,它看起来像这样

auto found=map.find(string_view_inst.getRef());

请注意,这对于c ++ 17 string_view类不起作用,因为它没有内部存储std :: string对象

ps。 您的string_view类可能不适用于cpu缓存,因为它存储指向堆中某个位置分配的字符串的指针,而字符串本身存储指向位于堆中其他位置的实际数据的指针。每次访问string_view都会导致双重取消引用。

答案 8 :(得分:-3)

您可以允许您的视图隐式转换为std::string

class StringView {
    // ...
    operator std::string() const
    {
        return data->substr(begin, len);
    }
    // ...
};