web-dev-qa-db-ja.com

std :: unordered_map :: Keyタイプとは異なるタイプを使用して検索しますか?

文字列型をキーとして使用する_unordered_map_があります。

_std::unordered_map<string, value> map;
_

_std::hash_スペシャライゼーションは、stringと、適切な_operator==_に提供されます。

これで、ヒープの割り当てを回避し、既存の文字列への弱いポインタである「文字列ビュー」クラスも作成されました。

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

ここで、_string_view_オブジェクトを使用して、キーがマップに存在するかどうかを確認できるようにしたいと思います。残念ながら、_std::unordered_map::find_は、汎用のKey引数ではなく、T引数を取ります。

(確かに、1つをstringに「昇格」することはできますが、それによって割り当てを回避したいのです。)

代わりに私が好きだったのは

_template<class Key, class Value>
class unordered_map
{
    template<class T> iterator find(const T &t);
};
_

これには、operator==(T, Key)std::hash<T>()を適切に定義する必要があり、イテレータを一致する値に戻します。

回避策はありますか?

29
peppe

上記のように、C++ 14はstd::unordered_mapの異種ルックアップを提供しません(std::mapとは異なります)。 Boost.MultiIndexを使用して、std::unordered_mapのかなり近い代替を定義できます。これにより、一時的なstring_viewsを割り当てずにstd::stringsを検索できます。

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

C++ 14が、基本的なmapでさえ、比較でis_transparentタイプのそのようなテンプレート化された検索を取得したのはごく最近のようです。ほとんどの場合、ハッシュされたコンテナの正しい実装はすぐには明らかになりませんでした。

私が見る限り、あなたの2つの選択肢は次のとおりです。

5
Mark B

P0919R2順序付けされていないコンテナの異種ルックアップ 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);
5
peppe

Githubで見つけたバリエーションを1つだけ紹介します。これには、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

0
v.oddou

さらに別のオプションは、複数のコンテナーを使用してルックアップとデータ管理を分割することです。

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)

mapKeyStorestd::stringを使用する方がはるかに直感的です。ただし、std::stringを使用しても、文字列メモリが変更されないことは保証されません(たとえば、ベクトルのサイズが変更された場合)。 unique_ptrを使用すると、これが強制されます。ただし、この例ではconvと呼ばれる特別な変換/割り当てルーチンが必要です。移動中のデータの整合性を保証する(そしてベクターに移動の使用を強制する)カスタム文字列コンテナーがある場合は、ここで使用できます。

欠点

上記の方法の欠点は、単純な方法で削除を行うと、削除の処理が簡単ではなく費用がかかることです。マップが1回だけ作成されるか、拡大するだけの場合、これは問題ではなく、上記のパターンは非常にうまく機能します。

実行例

以下の例には、1つのキーの単純な削除が含まれています。

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

もちろん、2つのコンテナーは、挿入と削除を独自に処理するラッパーに入れることができます。

0

このソリューションには欠点があり、コンテキストで実行できない場合とできない場合があります。

あなたはラッパークラスを作ることができます:

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

そして、str_wrapperをキーとして使用するようにマップを変更します。 2つのコンストラクターをstr_wrapperに追加する必要があります。1つはstd :: string用で、もう1つはstring_view用です。主な決定は、これらのコンストラクターに深いコピーを実行させるか、浅いコピーを実行させるかです。

たとえば、std :: stringを挿入にのみ使用し、str_viewをルックアップにのみ使用する場合、std :: stringコンストラクターを深くし、str_viewを浅くします(これは、カスタムラッパーを使用する場合、コンパイル時に適用できます。 unordered_map)。ディープコピーでのメモリリークを回避したい場合は、適切な破棄をサポートするために追加のフィールドが必要になります。

使用法がより多様である場合(std :: stringを検索するか、str_viewで挿入する)、欠点があります。これもまた、アプローチが不快になりすぎて実行不可能になる可能性があります。使用目的によって異なります。

0
dshin