5

Is there any nice way to use an unordered_map so that you can access objects by a member variable in constant time (average case)? The following example has this functionality but requires the name of each Person to be duplicated as the Key:

#include <iostream>
#include <string>
#include <unordered_map>
#include <algorithm>

class Person {
 public:
  Person() : name_("") {}
  Person(const std::string& name) : name_(name) {}
  std::string getName() const { return name_; }
  void kill() const { std::cout << name_ << " is dead!" << std::endl; }
 private:
  std::string name_;
};

int main(int argc, const char* argv[]) {
  Person p1("dave");
  Person p2("bob");

  std::unordered_map<std::string, Person> map = {
    {p1.getName(), p1}, // Duplicating the
    {p2.getName(), p2}  // keys here
  };

  map["dave"].kill();
  return 0;
}

I'm thinking that somehow the value_type would need to be Person itself, instead of a pair<string, Person> and the unordered_map would need to know to use Person::getName when hashing and accessing objects.

The ideal solution would allow me to set up an unordered_map (or unordered_set if it's more apt for the job) that knows to use Person::getName to get the key of each object. I would then be able to insert them simply by giving the object (and no key because it knows how to get the key) and access them by giving keys that would compare equal to the return value of Person::getName.

Something along the lines of:

// Pseudocode
std::unordered_map<Person, Person::getName> map = {p1, p2};
map["dave"].kill();

So is it possible to instantiate an unordered_map template class that can do this neatly?

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Joseph Mansfield
  • 108,238
  • 20
  • 242
  • 324
  • 1
    I don't know whether I understood the question correctly.. is `unordered_set` is the container you are looking for? – Naveen Aug 16 '11 at 08:44
  • I edited the question to give an idealistic piece of code. I don't believe `unordered_set` meets this spec (at least not with its most basic usage). – Joseph Mansfield Aug 16 '11 at 08:56
  • Call me crazy, but how about using a Boost.bimap where you make the right-hand side an `unordered_multimap`? Then you get right-to-left lookup in the complexity of that container. – Kerrek SB Aug 16 '11 at 11:50

3 Answers3

3

If you're not opposed to using Boost, then Boost.MultiIndex makes this extremely simple without adding any needless inefficiency. Here's an example that effectively creates an unordered_set of Person objects that is keyed on the value of Person::getName():

#include <string>
#include <iostream>
#include <boost/multi_index_container.hpp>
#include <boost/multi_index/indexed_by.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/mem_fun.hpp>

namespace bmi = boost::multi_index;

struct Person
{
    Person() = default;
    Person(std::string const& name) : name_(name) { }
    std::string const& getName() const noexcept { return name_; }
    void kill() const { std::cout << name_ << " is dead!\n"; }

private:
    std::string name_;
};

typedef bmi::multi_index_container<
    Person,
    bmi::indexed_by<
        bmi::hashed_unique<
            bmi::const_mem_fun<Person, std::string const&, &Person::getName>
        >
    >
> PersonUnorderedSet;

int main()
{
    Person p1("dave");
    Person p2("bob");

    PersonUnorderedSet set;
    set.insert(p1);
    set.insert(p2);

    set.find("dave")->kill();

    // for exposition, find is actually returning an iterator
    PersonUnorderedSet::const_iterator iter = set.find("bob");
    if (iter != set.end())
        iter->kill();

    // set semantics -- newly_added is false here, because
    // the container already contains a Person named 'dave'
    bool const newly_added = set.insert(Person("dave")).second;
}

(Note that I changed the signature of Person::getName() to return by const-reference rather than by value for efficiency's sake, but the change isn't strictly required.)


It should be noted that C++14's support for transparent comparators would allow you to use std::unordered_set<Person> here without any need for Boost.

Community
  • 1
  • 1
ildjarn
  • 62,044
  • 9
  • 127
  • 211
2

The obvious solution is to just duplicate the key part, and use std::unordered_map; for small, localized use, this is the simplest and the preferred solution, but there is nothing enforcing the invariant key == mapped.key_part, which make the code harder to maintain when insertions may occur at several different places in the code.

If you're keeping pointers in the map, rather than values, then you can wrap the map to arrange for the key to be a pointer to the appropriate field in the value. The problem with this is, of course, that it means keeping pointers, and not values.

If you don't modify the values once they're in the map, you can just use std::set, rather than std::map, with an appropriate hash and equality function. If you do this, you'll probably have to construct a dummy Person object in order to access the data.

If you want to modify the values (excluding the key, of course), then you have the problem that std::set only provides const access to its members. You can get around this using const_cast, but it's ugly. (And error prone; how do you ensure that the actual key part isn't modified.)

None of this solutions is entirely satisfactory. In a case like yours, where there's no mutable access to the key part (the name), I'd probably go with one of the first two solutions, wrapping the unordered_map in a class of my own to ensure that the invariants were maintained.

James Kanze
  • 150,581
  • 18
  • 184
  • 329
1

What you're looking for is an unordered_set, not a map. The set uses the value as both key and value.

Just don't change the "key" part of the value.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Lookup in an `unordered_set` is `O(1)` average case but `O(b.size())` worst case. – Lightness Races in Orbit Aug 16 '11 at 08:48
  • Wouldn't I then have to access the values in the set by using the values themselves? Ideally, I'd like to access them by member. i.e. rather than map[p1], map["dave"]. Unless there's a nice way of instantiating an unordered_set that does this. – Joseph Mansfield Aug 16 '11 at 08:50
  • @Tomalak Average case is what I'm going for. (I added that detail to the question) – Joseph Mansfield Aug 16 '11 at 09:01
  • @sftrabbit, the lookup is controlled by how you hashing works, if you've provided a hasher for `Person`, you'd need to construct a dummy Person object with the key(s) used by the hasher. The lookup will happen using this dummy and you'll get an iterator to the real instance. So your lookup would be `some_set.find(Person("dave"));` assuming that the hasher only works on the name attribute of Person. But the important point as Nicol says is, *don't change the key!* – Nim Aug 16 '11 at 09:26
  • 2
    `unordered_set` provides no write access to the objects it contains, so you can't modify the key (or anything else) without a `const_cast`. – James Kanze Aug 16 '11 at 09:45