【问题标题】:std::unordered_map::find using a type different than the Key type?std::unordered_map::find 使用不同于 Key 类型的类型?
【发布时间】:2016-01-04 17:42:12
【问题描述】:

我有一个使用字符串类型作为键的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&lt;T&gt;(),并将迭代器返回到匹配值。

有什么解决办法吗?

【问题讨论】:

  • 您能否更改您的程序以使用自定义的string 类来管理自己的字符串/堆分配?
  • 虽然我正在考虑答案,但只是一个小提示 - 您的 string_view 课程通常称为 string_ref
  • 为什么不向string_view 中引用datastd::string 引用提供转换运算符? operator std::string&amp;() { return *data; }那就没有分配了。
  • @PaulMcKenzie 大概string_view 代表字符串的一部分,而不是全部。
  • 无序关联容器没有异构查找。在某些实现中,hash&lt;int&gt;hash&lt;short&gt; 针对相同的值返回不同的结果。

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


【解决方案1】:

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

摘要似乎与 w.r.t 完美匹配。我原来的问题:-)

摘要

此提议为 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);

【讨论】:

    【解决方案2】:

    如上所述,C++14 不提供对std::unordered_map 的异构查找(与std::map 不同)。您可以使用 Boost.MultiIndex 为 std::unordered_map 定义一个相当接近的替代品,它允许您在不分配临时 std::strings 的情况下查找 string_views:

    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
    

    【讨论】:

    • 感谢您提供准确的unlike map,因为这就是我在这里的原因。我以为这个问题早就解决了。然后在这里我遇到了unordered 的构建问题,只是看到它已经解决了一半。谢谢
    【解决方案3】:

    看起来直到 C++14 才在比较中甚至基本的 map 获得了 is_transparent 类型的模板化查找。散列容器的正确实现很可能不是立即显而易见的。

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

    【讨论】:

    • boost::multi_index 实际上不维护两个索引吗?这似乎比分配和销毁新字符串的开销要大得多,尽管我同意 OP 的观点,即分配是没有意义的。
    • 请参阅我关于使用 Boost.MultiIndex 解决此问题的答案:它只使用一个索引,因此内存开销与 std::unordered_map 相同。
    【解决方案4】:

    我也面临同样的问题。

    我们需要两个结构体:

    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,示例:godbolt.org/z/8c8nYb 但我的示例仅适用于支持 cpp20 和此功能的编译器(目前这只是 msvc)
    • 该问题明确标记为 c++11。
    【解决方案5】:

    此解决方案有缺点,可能会或可能不会使其不适合您的上下文。

    你可以做一个包装类:

    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 插入),就会有缺点,这再次可能使该方法太令人反感,以至于不可行。这取决于您的预期用途。

    【讨论】:

      【解决方案6】:

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

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

      使用string_views 完成查找,无需分配。 每当插入新键时,我们需要先添加一个真正的字符串分配:

      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)
      

      mapKeyStore 中使用std::string 会更直观。但是,使用 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;
      }
      

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

      【讨论】:

        【解决方案7】:

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

        原文:
        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

        【讨论】:

          【解决方案8】:

          很抱歉回答这个非常老的问题,但它仍然出现在搜索引擎结果中...... 在这种情况下,您的 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 时,都会导致双重取消引用。

          【讨论】:

          • 仔细查看 OP 的代码。除了string *,OP 的string_view 还存储了size_t begin, len;。他们似乎想使用指向字符串的子字符串(从 begin 开始,len 字符长),而不是整个字符串。
          • 哎呀,错过了。
          • 不仅如此,string_views 还可以包装 static char const* 甚至是子范围。
          【解决方案9】:

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

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

          【讨论】:

          • OP 解释说他们不想要分配。
          猜你喜欢
          • 2013-03-26
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-09-10
          • 2021-10-07
          • 2017-04-27
          • 1970-01-01
          相关资源
          最近更新 更多