How to declare/use an unordered_set for triplets (tuple) using custom comparator?
I need to store triplets of float (handled as tuple) in a set to check for potential duplicates. As it’s about float, I guess using regular compare with == will not work so customizing compare is needed.
This minimal code doesn’t compile:
>> cat unordered_set_triplet.cpp
#include <unordered_set>
#include <tuple>
#include <limits> // numeric_limits
#include <cmath> // fabs
#include <functional> // hash
using triplet = std::tuple<float, float, float>;
bool triplet_equal(triplet const & lhs, triplet const & rhs) {
float const eps = std::numeric_limits<float>::epsilon();
if (std::fabs(std::get<0>(lhs) - std::get<0>(rhs)) > eps) return false;
if (std::fabs(std::get<1>(lhs) - std::get<1>(rhs)) > eps) return false;
if (std::fabs(std::get<2>(lhs) - std::get<2>(rhs)) > eps) return false;
return true;
}
using unordered_set_triplet = std::unordered_set<triplet,
std::hash<triplet>,
decltype(triplet_equal)>;
int main() {
//unordered_set_triplet s; // Compilation: KO...
unordered_set_triplet s(10, std::hash<triplet>, triplet_equal);
s.insert({1.f, 2.f, 3.f});
}
I get:
>> g++ -std=c++20 -o unordered_set_triplet unordered_set_triplet.cpp
In file included from /usr/include/c++/12/bits/hashtable.h:35,
from /usr/include/c++/12/unordered_set:46,
from unordered_set_triplet.cpp:1:
/usr/include/c++/12/bits/hashtable_policy.h: In instantiation of ‘struct std::__detail::_Hashtable_ebo_helper<0, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&), false>’:
/usr/include/c++/12/bits/hashtable_policy.h:1631:12: required from ‘struct std::__detail::_Hashtable_base<std::tuple<float, float, float>, std::tuple<float, float, float>, std::__detail::_Identity, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&), std::hash<std::tuple<float, float, float> >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Hashtable_traits<true, true, true> >’
/usr/include/c++/12/bits/hashtable.h:182:11: required from ‘class std::_Hashtable<std::tuple<float, float, float>, std::tuple<float, float, float>, std::allocator<std::tuple<float, float, float> >, std::__detail::_Identity, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&), std::hash<std::tuple<float, float, float> >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >’
/usr/include/c++/12/bits/unordered_set.h:100:18: required from ‘class std::unordered_set<std::tuple<float, float, float>, std::hash<std::tuple<float, float, float> >, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&)>’
unordered_set_triplet.cpp:21:26: required from here
/usr/include/c++/12/bits/hashtable_policy.h:1204:11: error: data member ‘std::__detail::_Hashtable_ebo_helper<0, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&), false>::_M_tp’ invalidly declared function type
1204 | _Tp _M_tp{};
| ^~~~~
/usr/include/c++/12/bits/hashtable.h: In instantiation of ‘class std::_Hashtable<std::tuple<float, float, float>, std::tuple<float, float, float>, std::allocator<std::tuple<float, float, float> >, std::__detail::_Identity, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&), std::hash<std::tuple<float, float, float> >, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, true, true> >’:
/usr/include/c++/12/bits/unordered_set.h:100:18: required from ‘class std::unordered_set<std::tuple<float, float, float>, std::hash<std::tuple<float, float, float> >, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&)>’
unordered_set_triplet.cpp:21:26: required from here
/usr/include/c++/12/bits/hashtable.h:665:7: error: function returning a function
665 | key_eq() const
| ^~~~~~
In file included from /usr/include/c++/12/unordered_set:47:
/usr/include/c++/12/bits/unordered_set.h: In instantiation of ‘class std::unordered_set<std::tuple<float, float, float>, std::hash<std::tuple<float, float, float> >, bool(const std::tuple<float, float, float>&, const std::tuple<float, float, float>&)>’:
unordered_set_triplet.cpp:21:26: required from here
/usr/include/c++/12/bits/unordered_set.h:632:7: error: function returning a function
632 | key_eq() const
| ^~~~~~
unordered_set_triplet.cpp: In function ‘int main()’:
unordered_set_triplet.cpp:21:49: error: expected primary-expression before ‘,’ token
21 | unordered_set_triplet s(10, std::hash<triplet>, triplet_equal);
| ^
How to fix this?
EDIT
Doesn’t work either using (ordered) set:
>> cat set_triplet.cpp
#include <iostream>
#include <set>
#include <tuple>
#include <limits> // numeric_limits
#include <cmath> // fabs
#include <functional> // hash
using triplet = std::tuple<float, float, float>;
bool triplet_equal(triplet const & lhs, triplet const & rhs) {
float const eps = std::numeric_limits<float>::epsilon();
if (std::fabs(std::get<0>(lhs) - std::get<0>(rhs)) > eps) return false;
if (std::fabs(std::get<1>(lhs) - std::get<1>(rhs)) > eps) return false;
if (std::fabs(std::get<2>(lhs) - std::get<2>(rhs)) > eps) return false;
return true;
}
using set_triplet = std::set<triplet, std::hash<triplet>, decltype(triplet_equal)>;
int main() {
//set_triplet s; // Compilation: KO...
set_triplet s(10, std::hash<triplet>, triplet_equal);
s.insert({1.f, 2.f, 3.f});
s.insert({1.0000001f, 2.0000001f, 3.0000001f});
for (auto const & t : s) std::cout << std::get<0>(t) << ", " << std::get<1>(t) << ", " << std::get<2>(t) << std::endl;
}
What container could be appropriate to use?
triplet can be seen a 3D point (XYZ): I need to handle/detect duplicate points.
>Solution :
Problems with KeyEqual
Firstly, the KeyEqual provided to the std::unordered_set can’t be a function, and that’s what you’re trying to do with decltype(triplet_equal). However, it could be a function pointer. Normally, you should use a function object as follows:
struct triplet_equal {
// note: static constexpr only since C++23, otherwise remove those two
static constexpr bool operator()(triplet const & lhs, triplet const & rhs) const {
float const eps = std::numeric_limits<float>::epsilon();
if (std::fabs(std::get<0>(lhs) - std::get<0>(rhs)) > eps) return false;
if (std::fabs(std::get<1>(lhs) - std::get<1>(rhs)) > eps) return false;
if (std::fabs(std::get<2>(lhs) - std::get<2>(rhs)) > eps) return false;
return true;
}
};
// ...
std::unordered_set<triplet, std::hash<triplet>, triplet_equal> s(10);
You don’t have to provide any value for the hash or for triplet_equal to the constructor because they are default-constructible.
Problems with Hash
The next huge issue is that the standard library has no std::hash specialization for tuples. Look into Generic hash for tuples in unordered_map / unordered_set if you want to make your own.
However, even if there was one, the problem remains that two equal tuples (where equality is determined by triplet_equal) must also have the same hash. You would have to specialize std::hash yourself so that two equal tuples always have the same hash, despite floating-point imprecision. You might be able to do it by quantizing floats, but I imagine it would be very difficult to do correctly.
Alternative: use std::set and provide a Compare
It would be much easier to use std::set, which only requires you to implement a less-than comparison:
struct triplet_less {
constexpr bool operator()(triplet const & lhs, triplet const & rhs) const {
float const eps = std::numeric_limits<float>::epsilon();
float d0 = std::get<0>(lhs) - std::get<0>(rhs);
if (d0 < -eps) return true;
if (d0 > eps) return false;
float d1= std::get<1>(lhs) - std::get<1>(rhs);
if (d1 < -eps) return true;
if (d1 > eps) return false;
float d2 = std::get<2>(lhs) - std::get<2>(rhs);
if (d2 < -eps) return true;
return false;
}
};
int main() {
std::set<triplet, triplet_less> s;
s.insert({1.f, 2.f, 3.f});
}
See live example at Compiler Explorer
Further Notes
It would be much better not to use std::tuple, but use a simple aggregate type as follows:
struct vec3 {
float x, y, z;
// C++20: default all comparison operators
// (you still need a custom vec3_equal to deal with precision issues)
friend auto constexpr operator<=>(vec3 const&, vec3 const&) = default;
};
With defaulted comparison operators, it’s very easy to get all the functionality of std::tuple, and you can write lhs.x instead of needing std::get<0>(lhs) and other annoyances.