使用与键类型不同的类型在std::unordered_map中查找元素:find函数?

49

我有一个使用字符串类型作为键的 unordered_map

std::unordered_map<string, value> map;

对于string,提供了一个std::hash的专门实现,以及一个合适的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>(),并返回指向匹配值的迭代器。

是否有任何解决方法?


当我在思考答案时,只是一个小提示 - 你的 string_view 类通常被称为 string_ref - SergeyA
1
为什么不在string_view中提供一个转换运算符,将std::string引用指向data呢? operator std::string&() { return *data; } 这样就不需要分配内存了。 - PaulMcKenzie
2
@PaulMcKenzie 可能 string_view 表示字符串的一部分,而不是整个字符串。 - T.C.
1
无序关联容器没有异构查找。在某些实现中,hash<int>hash<short>对于相同的值返回不同的结果。 - T.C.
@T.C. 好的,我明白了,如果它代表的是部分字符串而不是完整字符串,那就有问题了。 - PaulMcKenzie
显示剩余7条评论
9个回答

39

P0919R2 异构查找用于无序容器已经合并到C++2a的工作草案中!

摘要似乎与我的原始问题完美匹配 :-)

Abstract

This proposal adds heterogeneous lookup support to the unordered associative containers in the C++ Standard Library. As a result, a creation of a temporary key object is not needed when different (but compatible) type is provided as a key to the member function. This also makes unordered and regular associative container interfaces and functionality more compatible with each other.

With the changes proposed by this paper the following code will work without any additional performance hits:

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);

来自cppreference的完整示例

#include <cstddef>
#include <functional>
#include <iostream>
#include <string>
#include <string_view>
#include <unordered_map>
 
using namespace std::literals;
 
struct string_hash
{
    using hash_type = std::hash<std::string_view>;
    using is_transparent = void;
 
    std::size_t operator()(const char* str) const        { return hash_type{}(str); }
    std::size_t operator()(std::string_view str) const   { return hash_type{}(str); }
    std::size_t operator()(std::string const& str) const { return hash_type{}(str); }
};
 
int main()
{
    // simple comparison demo
    std::unordered_map<int,char> example = {{1, 'a'}, {2, 'b'}};
 
    if (auto search = example.find(2); search != example.end())
        std::cout << "Found " << search->first << " " << search->second << '\n';
    else
        std::cout << "Not found\n";
 
    // C++20 demo: Heterogeneous lookup for unordered containers (transparent hashing)
    std::unordered_map<std::string, size_t, string_hash, std::equal_to<>> map{{"one"s, 1}};
    std::cout << std::boolalpha
        << (map.find("one")   != map.end()) << '\n'
        << (map.find("one"s)  != map.end()) << '\n'
        << (map.find("one"sv) != map.end()) << '\n';
}

15
如上所述,C++14不支持std::unordered_map的异构查找(与std::map不同)。您可以使用Boost.MultiIndex来定义一个相当接近于std::unordered_map的替代方案,它允许您查找string_view而无需分配临时std::string

Live Coliru演示

#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
感谢您明确提到“不同于map”,这正是我在这里的原因。我以为那个问题早就解决了。但现在我又遇到了使用“unordered”时的构建问题,结果发现只是半解决了。谢谢。 - v.oddou

9

我遇到了同样的问题。

我们需要两个结构体:

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>;

对于 unordered_set:

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

现在可以使用string_view了。


string_unorderd_map.find(string_view) 不起作用。 - Spongman
但是我的示例仅适用于支持cpp20和此功能的编译器(目前只有msvc)。 - Nikita Avdosev
1
这个问题明确标记了C++11。 - Spongman

5
看起来直到C++14,即使是基本的map也只有在比较中使用了is_transparent类型的模板化查找。很可能哈希容器的正确实现并不明显。
据我所见,你有两个选择:
- 只需进行分配和性能测试,以确定它是否真的是一个问题。 - 查看boost::multi_indexhttp://www.boost.org/doc/libs/1_60_0/libs/multi_index/doc/index.html),并将stringstring_view索引到容器中。

1
boost::multi_index 实际上并维护两个索引吗?这似乎比分配和销毁新字符串要多得多,尽管我同意 OP 的看法,即分配是无意义的。 - rici
请查看我关于使用Boost.MultiIndex解决此问题的答案:它仅使用一个索引,因此内存开销与std::unordered_map相同。 - Joaquín M López Muñoz

1
另一种选择是通过使用多个容器来分离查找和数据管理:
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::stringmapKeyStore中会更加直观。然而,使用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;
}

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

1
这个解决方案有缺点,可能会使它在你的情境下不可行。您可以创建一个包装类:
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插入),则会有缺点,这可能再次使该方法过于不受欢迎而无法使用。 这取决于您的预期使用方式。


0
我只是在GitHub上发现了一个变体,它涉及定义一个新的地图类,该类包装了std。
重新定义一些关键API以拦截我们想要的适配器,并使用静态字符串复制键。
这不是必须的好解决方案,但对于认为足够的人来说,知道它的存在是有趣的。

original:
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


对我来说,它看起来不是线程安全的。Find函数可能会被操作系统中断,并且另一个线程可能会在执行相同代码并使用相同的std::string缓冲区的同一线程中调度。 - Sergey

-3

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

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

如果要在 map 中使用 string_view,代码应该如下所示

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

请注意,这对于C++17的string_view类不起作用,因为它不会在内部存储std::string对象。
另外,你的string_view类可能不适合CPU缓存,因为它存储一个指向堆上分配的字符串的指针,而字符串本身存储一个指向实际数据的指针,在每次访问string_view时都需要进行两次解引用。

1
仔细查看 OP 的代码。除了 string * 之外,OP 的 string_view 还存储了 size_t begin, len;。他们似乎想要使用指向字符串的子串(从 begin 开始,长度为 len),而不是整个字符串。 - HolyBlackCat
糟糕,错过了。 - Medran
不仅如此,string_view还可以包装static char const*甚至子范围。 - v.oddou

-5

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

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

3
OP解释说他们不想要分配。 - SergeyA

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