"std::unordered_map",无需复制关键数据
`std::unordered_map` without duplicating key data
我有一个Person
类,它有一个name
属性(std::string
(。
我想创建一个查找表,一个std::unordered_map
,这样我就可以通过他们的名字找到一个Person
。但是,给定一个Person
,我也希望能够得到他们的名字。
这需要将name
存储两次 - 一次作为映射的键,一次在 person 对象中,如下面的代码所示。
由于我一次将许多Person
加载到内存中,因此我不希望两次存储其名称的开销。
我尝试在Person
类中使用指向键的引用/指针,但这会产生问题,因为映射在修改时似乎会重新洗牌其数据,并且引用变得无效。
我也尝试过使用std::unordered_set
,但这意味着每次我想执行查找时都需要构造一个完整的Person
对象。
无序列图的键和值有什么方法可以共享相同的数据?
#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;
}
您可以使用 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 &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;
注意:您可以将存储的数据更改为共享指针,而不是按值存储当然,我只是省略了它,例如简单性。
使用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
演示
如果您的"人员"从未被复制或移动,并且他们的姓名从未被复制或移动,则可以使用指向string
的指针而不是string
作为键。这需要使用自定义hash
和equal
函子。
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
内联,因为您的代码必须获取指向它的指针。
如果你真的在为记忆而苦苦挣扎,你应该使用boost::flat_set
. 它的内存开销非常低,唯一的问题是,如果您更新一组人员,它的性能会很糟糕。 如果您只是创建并且从不修改它,那么性能比unordered_
差,但并不可怕。
如果您坚持使用unordered_map
我认为您需要使用 unordered_multiset因为我认为让您的类仅使用一个字段来确定 2 个实例是否相等是没有意义的。 这是可能的,但非常丑陋,你需要定义自己的哈希和相等函数。
另一个更简单但更容易出错的解决方案是使用哈希作为键,如下所示:
#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
。
- C++17复制构造函数,在std::unordereded_map上进行深度复制
- 在C++程序中输入的文本文件将不起作用,除非文本被复制和粘贴
- 使用strcpy将char数组的元素复制到另一个数组
- 是否可以初始化不可复制类型的成员变量(或基类)
- 为什么在C++中使用私有复制构造函数与删除复制构造函数
- C++ Windows 驱动程序MSB3030无法复制该文件,因为它找不到
- 复制列表初始化的隐式转换的等级是多少
- 当从函数参数中的临时值调用复制构造函数时
- 有可能在Armadillo中复制MATLAB circshift方法吗
- 复制几乎为空的数组的最快方法
- 以下示例中如何避免代码复制?C++/库达
- 如果有一个模板构造函数只有一个泛型参数,为什么我必须有一个复制构造函数
- 为什么需要复制构造函数,在哪些情况下它们非常有用
- 不能将复制初始化与隐式转换的多个步骤一起使用
- 当有分配器意识的容器被复制/移动时,反弹分配器是否被复制/移走
- 为什么复制而不是移动数据元素?
- 文件系统:复制功能的速度秘诀是什么
- 使用仅使用一次的变量调用的复制构造函数.这可能是通过调用move构造函数进行编译器优化的情况吗
- 为什么类中的ostringstream类型的成员会导致";调用隐含删除复制构造函数";错误
- 使lambda不可复制/不可移动