【问题标题】:`std::unordered_map` without duplicating key data`std::unordered_map` 不复制关键数据
【发布时间】:2017-08-24 13:11:45
【问题描述】:

我有一个 Person 类,它有一个 name 属性 (std::string)。

我想创建一个查找表,一个std::unordered_map,所以我可以通过他们的名字找到一个Person。但是,给定一个Person,我也希望能够得到他们的名字。

这需要将name 存储两次 - 一次作为地图的键,一次在人员对象中,如下面的代码所示。

由于我一次将许多Persons 加载到内存中,因此我不希望将它们的名称存储两次的开销。

我尝试在 Person 类中使用指向键的引用/指针,但这会产生问题,因为地图在修改时似乎会重新排列其数据,并且引用变得无效。

我也尝试过使用std::unordered_set,但这意味着我每次想要执行查找时都需要构造一个完整的Person 对象。

有没有办法让无序映射的key和value共享相同的数据?

#include <iostream>
#include <unordered_map>


class Person
{
    private:
        const std::string _name;

    public:
        Person( const std::string& name ) : _name( name )
        {
        }


        const std::string& get_name() const
        {
            return _name;
        }
};


int main()
{
    auto my_set = std::unordered_map<std::string, std::shared_ptr<Person>>();

    my_set.insert( { "alice", std::shared_ptr<Person>( new Person( "alice" )) } );
    my_set.insert( { "bob", std::shared_ptr<Person>( new Person( "bob" )) } );
    my_set.insert( { "charlie", std::shared_ptr<Person>( new Person( "charlie" )) } );

    std::cout << my_set.find( "bob" )->second->get_name() << std::endl;

    return 0;
}

【问题讨论】:

  • 您必须将name 存储在Person 中吗?
  • 在这个假设的例子中,不,但假设我这样做。
  • @cz - 为什么不使用带有自定义哈希的无序集呢?无论如何,这似乎就是您所追求的。
  • 你也可以使用排序向量。
  • 除非 person 对象很大,否则使用unordered_set 可能是您最好的选择。它是节省空间的,仍然是 O(1)。默认构造一些字段,包括向量和字符串之类的东西,非常便宜。

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


【解决方案1】:

您可以为此目的使用Boost.Multi-index。虽然这个库有一个学习曲线,但您会发现它非常快速可用。所以对于你的情况:

namespace mpi = boost::multi_index;
boost::multi_index_container<
        Person,
        mpi::indexed_by<
           mpi::hashed_unique< mpi::const_mem_fun< Person, const std::string &, &Person::get_name > >
        >
> my_set;

现在您可以将其用作带有字符串键的散列集:

auto f = my_set.find( "bob" );
if( f != my_set.end() )
    std::cout << f->get_name() << std::endl; 

这可能看起来有点矫枉过正,但是当您开始向 Person 类添加更多成员时,您会看到这个库的全部功能,您需要提供不同的索引才能让该成员访问它们。假设您添加了一个唯一的电话号码(方法const std::string &amp;get_phone() const):

boost::multi_index_container<
        Person,
        mpi::indexed_by<
           mpi::hashed_unique< mpi::const_mem_fun< Person, const std::string &, &Person::get_name >,
           mpi::hashed_unique< mpi::const_mem_fun< Person, const std::string &, &Person::get_phone >>
        >
> my_set;

// lookup by phone:

const auto &idx = boost::get<1>( my_set );
auto f = idx.find( "1234567890" );
if( f != my_set.end() )
    std::cout << f->get_name() << std::endl; 

注意:当然,您可以将存储的数据更改为共享指针,而不是按值存储,我只是为了简单而省略了这一点。

【讨论】:

  • 老实说,我不确定这是否会比简单的unordered_map 解决方案使用更少的空间。一个字符串的大小通常为 3-4 个单词,因此这意味着 multi_index 的开销小于 4N 左右,其中 N 是条目数。
  • @NirFriedman 我怀疑 OP 主要关心的是实际空间,更大的数据重复问题,因为这会在程序中产生错误 - 在他的示例中,很容易存储名称与键不同的人。
  • 虽然这对您来说似乎不太可能......但您不能完全没有理由地提出第二个猜测问题。直接引用:“由于我有很多人一次加载到内存中,我不希望将他们的名字存储两次的开销。”。
  • @NirFriedman 您认为问题中的开销意味着内存开销,但这不是强制性的,是吗?这种方法有多种开销,而内存只有其中之一。
  • “数据重复”“错误开销”与创建的人员数量不成比例,所以说 OP 在该引用中谈论某种其他开销充其量是可疑的。在您与我争论的时间里,您本可以编写一个简单的 10 行分配器来显示正在使用的大量内存,并大大改善了您的答案。
【解决方案2】:

使用std::set,您可能会使用透明比较器(std::unordered_set 似乎不支持:/):

struct LessPerson
{
    using is_transparent = void; // enable "transparent" comparer

    template <typename T1, typename T2>
    bool operator ()(const T1& t1, const T2& t2) const
    {
        // Compare only "name".
        return toString(t1) < toString(t2);
    }

    // trivial one
    const std::string& toString(const std::string& s) const
    {
        return s;
    }

    // the one why we create the class
    const std::string& toString(const Person& p) const
    {
        return p.get_name();
    }

    // A tricky one to handle dereference of (smart) pointers.
    template <typename T,
              std::enable_if_t<std::is_same<Person, std::decay_t<decltype(*std::declval<T>())>>::value>* = nullptr>
    const std::string& toString(const T& p) const
    {
        return (*p).get_name();
    }

};

然后使用它:

auto my_set = std::set<std::shared_ptr<Person>, LessPerson>();

my_set.insert( { std::make_shared<Person>("alice") } );
my_set.insert( { std::make_shared<Person>("bob") } );
my_set.insert( { std::make_shared<Person>("charlie") } );

auto it = my_set.find("bob"); // search using "bob" directly without creating a new Person

Demo

【讨论】:

    【解决方案3】:

    如果您的“人员”从未被复制或移动,并且他们的名字从未被复制或移动,您可以使用指向string 的指针而不是string 作为您的键。这需要使用自定义的 hashequal 函子。

    struct myhash
    {
        unsigned operator()(std::string* s) const
        {
            return std::hash<std::string>()(*s);
        }
    };
    
    struct myequal
    {
        unsigned operator()(std::string* s1, std::string* s2) const
        {
            return *s1 == *s2;
        }
    };
    ...
    auto my_set = std::unordered_map<std::string*, std::shared_ptr<Person>, myhash, myequal>();
    

    这也使查找变得有点复杂:您必须查找指向string 的指针。

    std::string b = "bob";
    std::cout << my_set.find(&b)->second->get_name() << std::endl;
    

    这里不可能有字符串bob 内联,因为你的代码必须得到一个指向它的指针。

    【讨论】:

    • 你提到的缺点可能可以通过简单地使用reference_wrapper&lt;const std::string&gt;作为键来消除。
    • @Nir Friedman 不。其次,使用右值调用 .find 是不可能的,首先在复制对象和引用时,reference_wrapper 被简单地复制并复制了对象,包装器中的引用仍然指向原始字符串。
    • @DrSvanHay 你是对的,因为他们删除了那个重载,但是你自己写一些具有正确行为的东西是微不足道的。我根本不明白你的第二点,它在复制时的行为与答案使用的指针相同。
    【解决方案4】:

    如果您真的在记忆力方面遇到困难,您应该使用boost::flat_set。 它的内存开销非常低,唯一的问题是如果你更新你的一组人,它的性能很糟糕。如果你只是创建而不修改它,性能会比unordered_ 差,但并不糟糕。

    如果您坚持使用unordered_map,我认为您需要使用 unordered_multiset,因为我认为让您的班级仅使用一个字段来确定 2 个实例是否相等是没有意义的。 这是可能的,但是very ugly,您需要定义自己的散列和相等函数。

    另一个更简单但更容易出错的解决方案是像这样使用哈希作为键:

    #include <string>
    #include <iostream>
    #include <unordered_map>
    
    class Person {
    
    public:
        Person(const std::string& name, const int age) : name_(name), age_(age) {}
    public:
        const std::string& name() const { return name_; }
        int age() const { return age_; }
    private:
        std::string name_;
        int age_;
    };
    
    int main()
    {
        Person p1("Joe", 11), p2("Jane", 22), p3("James", 33), p4("Joe", 44);
        std::unordered_multimap<size_t, Person> persons{ {std::hash<std::string>()(p1.name()), p1}, {std::hash<std::string>()(p2.name()), p2},{std::hash<std::string>()(p3.name()), p3}, {std::hash<std::string>()(p4.name()), p4} };
        auto potential_joes = persons.equal_range(std::hash<std::string>()("Joe"));
        for (auto it = potential_joes.first; it != potential_joes.second; ++it) {
            if (it->second.name() == "Joe") {
                std::cout << it->second.name() << " is " << it->second.age() << " years old" << std::endl;
            }
        }
    }
    

    仅当您的字符串很长,您实际测量了内存使用量并且您对编写自定义比较器感到不舒服时,我才会使用它。 正如您从代码中看到的那样,您自己重新实现了很多unordred_map 逻辑,而且很容易搞砸。

    重要提示 如果您的键取决于您在地图中的值,您必须确保不要修改值。 因此,例如在我发布的代码中,您可能应该将成员设为 name_ const 并评论为什么它是 const

    【讨论】:

      猜你喜欢
      • 2011-12-28
      • 1970-01-01
      • 2014-01-11
      • 1970-01-01
      • 2012-07-09
      • 1970-01-01
      • 2016-08-29
      • 2013-11-05
      • 1970-01-01
      相关资源
      最近更新 更多