0

Introduction

Hello everyone, i try to use boost::unordered_set for a custom class type. The class stores information about coordinates and several other values but only the coordinates are used to create the hash value. Now if i want to insert a point and there is already a point with equal coordinates (hence a set) i need to change a third value from the original object (like object.isDuplicate = true very simplified). Please do not stick too much to the bool value and duplicate detection cause in the original code it is a bit more complex but it should only show that i need a non-const access to the stored class. I can only use boost 1.53 and C++03 and GCC 4.4.3

The problem

The problem is now when i try to insert a point with boost::unordered_set::insert i get a pair<iterator, bool> of which the first member is an immutable iterator to the inserted or original entry and the second is a bool indicating if the value was inserted or not. I can not change the value with an immutable iterator unfortunately so i had to think of something different. So i now try to store a pointer to my object in the set and then access it via this pointer to change the value (which should be okay since the value has nothing to do with the hash value and thus does not alter the key). So i tried to overload the boost::hash_value function to accept a pointer to my class like this:

size_t hash_value(const A * a) {
    size_t seed = 0;
    boost::hash_combine(seed, a->a);
    boost::hash_combine(seed, a->b);
    return seed;
}

But the unordered_set does not seem to use my overloaded function (i tried printing the seed at the end but it does not show up hence i assume it uses a different overload) even if i initialize my set with unordered_set< A *, boost::hash<A *> >. For the hashing aspect: when i try to use the set without a pointer it works fine but i can not alter the value.

Possible problem

I searched a bit around in the boost::hash reference and found this overload template<typename T> std::size_t hash_value(T* const&); which i think is used instead of my own one (and simply hashes with the objects address) but then i wonder why my compiler does not prompt a redefinition of this function (i compile with -Wall -Wextra -pedantic flags enabled.

Question

So is this the actual problem? And if it is how can i tell my compiler to explicitely use my custom hash function?

Code

At last a little example i wrote to test everything

#include <iostream>
#include <string>
#include <boost/functional/hash.hpp>
#include <boost/unordered_set.hpp>

using boost::unordered_set;

struct A {
    double a;
    double b;
    bool isDup;
    A(const double a, const double b): a(a), b(b), isDup(false) {}
    A(const A & a): a(a.a), b(a.b), isDup(a.isDup) {}
    
    /* Two equal As ought to have a bitwise equal floating point value so this is okay */
    bool operator==(const A & a) const {
        if (a.a != this->a) return false;
        if (a.b != this->b) return false;
        return true;
    }
};


size_t hash_value(const A * a) {
    size_t seed = 0;
    boost::hash_combine(seed, a->a);
    boost::hash_combine(seed, a->b);
    std::cout << "Seed: " << seed << std::endl; /* This is not printed so i assume the function is not called */
    return seed;
}


int main() {
    A a1(1.2, 2.3);
    A a2(2.3, 3.4);
    A a3(3.4, 4.5);
    A a4(a1);
    
    unordered_set< A *, boost::hash<A *> > usa; /* This was unintended lol */
    if ( ! usa.insert(&a1).second ) std::cout << "Error " << a1.a << ", " << a1.b << " is already in set" << std::endl;
    if ( ! usa.insert(&a2).second ) std::cout << "Error " << a2.a << ", " << a2.b << " is already in set" << std::endl;
    if ( ! usa.insert(&a3).second ) std::cout << "Error " << a3.a << ", " << a3.b << " is already in set" << std::endl;
    if ( ! usa.insert(&a4).second ) {
        /* This is not called */
        std::cout << "Error " << a4.a << ", " << a4.b << " is already in set" << std::endl;
        (*(usa.insert(&a4).first))->isDup = true;
    }
}
Community
  • 1
  • 1
Yastanub
  • 1,227
  • 8
  • 19

2 Answers2

2

There are a couple of issues with your original function hash_value:

  1. It must be inside boost namespace because boost::hash<T*> invokes boost::hash_value which disables argument-dependent name lookup.
  2. In templates name lookup is performed twice: at declaration and instantiation time. At instantiation time only argument-dependent name lookup is performed but it is disabled by 1. This is why your hash function must be declared before the definition of boost::hash (before including boost/hash.hpp).

E.g.:

#include <cstddef> // std::size_t

struct A;
namespace boost { inline std::size_t hash_value(A* a); }

#include <iostream>
#include <string>
#include <boost/functional/hash.hpp>
#include <boost/unordered_set.hpp>

struct A { /*... */};

size_t boost::hash_value(A* a) {
    size_t seed = 0;
    boost::hash_combine(seed, a->a);
    boost::hash_combine(seed, a->b);
    std::cout << "Seed: " << seed << std::endl; /* This is not printed so i assume the function is not called */
    return seed;
}

Also, you need to specify your own element comparison class, the default one in boost::unordered_set compares pointers.


As a side note the design of boost::hash and std::hash is less than ideal in respect of combining hashes of multiple members. I cannot recommend enough using the new hash framework from N3980 Types Don't Know #.

Maxim Egorushkin
  • 131,725
  • 17
  • 180
  • 271
  • Thanks i noticed the problem with the comparator too as stated in my own answer. But your solution does not seem to work for me as well. I declared the function in `boost` namespace and then created my set with `unordered_set, compA> usa` but it does not work either. What do i have to pass for the second template argument? Also thanks for the link i will take a look at it. – Yastanub Nov 26 '18 at 16:53
  • @Yastanub I compiled it and it works. What do you mean by _does not work either_? – Maxim Egorushkin Nov 26 '18 at 16:54
  • Sry it works now. I still had `boost::hash_value(const A *)` as my signature when i make the parameter non const it works. Thanks alot :) – Yastanub Nov 26 '18 at 16:55
  • But i must say i do not really like this declaration very well because i only need the hash function in a single place inside one function. Is there an advantage over a locally defined `class comparator` like i defined in [my answer](https://stackoverflow.com/a/53485281/9232218) or can i just use the local class? – Yastanub Nov 26 '18 at 16:59
  • @Yastanub I do not like it either, and this is a part of the reason I don't use `std/boost::hash`. If you store `A` by value that removes these problems for you. Or specify your custom comparison and hash classes, like you did. – Maxim Egorushkin Nov 26 '18 at 17:02
  • I can not simply store `A` by value since i need to modify it and there is only a const access to set keys. But as i am not modifying something that has to do with the hash value, it should be okay to modify it right? – Yastanub Nov 27 '18 at 13:53
  • @Yastanub Cast away constness and modify the members, as long as they are not part of the object key. This is safe to do, since the objects in the container are not originally created as `const` (they are created by the allocator, which allocates them on the heap). Alternatively, make those members `mutable` but that is a bit of an overkill, IMO. They make set elements `const` to prevent an accidental key change that could corrupt the container. – Maxim Egorushkin Nov 27 '18 at 13:58
0

Okay i found a solution (or a workaround rather?) by myself now. A second problem was the equal_to class which is used by default by boost::unordered_set. equal_to<A *> would never return false because we always have distinct points and thus &a1 == &a2 would always evaluate to false so i had to write my own comparator as well which dereferences the objects before comparing them and then invoces their operator==.

Then I simply encapsulated the hash function and the comparator in a separate class and then pass them as template arguments when creating the set like this:

class compA {
public:
    size_t operator()(const A * a) const {
        size_t seed = 0;
        boost::hash_combine(seed, a->a);
        boost::hash_combine(seed, a->b);
        return seed;
    }

    bool operator()(const A * a1, const A * a2) const {
        if (*a1 == *a2) return true;
        return false;
    }
};

unordered_set<A *, compA, compA> usa;

But i still would like to know why my initial attempt did not work.

Yastanub
  • 1,227
  • 8
  • 19