Como declarar/usar um unordered_set
para trigêmeos ( tuple
) usando comparador personalizado?
Preciso armazenar trigêmeos de float
(tratados como tuple
) em um conjunto para verificar possíveis duplicatas. Como se trata de float
, acho que usar comparação regular com ==
não funcionará, portanto, é necessário personalizar a comparação.
Este código mínimo não compila:
>> 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});
}
Eu recebo:
>> 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);
| ^
Como consertar isto?
EDITAR
Também não funciona usando (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;
}
Qual recipiente poderia ser apropriado para uso?
triplet
pode ser visto um ponto 3D (XYZ): preciso manipular/detectar pontos duplicados.
MELHOR SOLUÇÃO ATÉ AGORA
Usando tuplas feitas de números inteiros i
construídos desta forma i = (int) 1000000 * f
a partir de float f
e usando set (pois operator<
respeitará a ordem estrita com precisão de até 6 dígitos após a multiplicação por 1.000.000).
>> cat set_uint32_triplet.cpp
#include <iostream>
#include <set>
#include <tuple>
using triplet_uint32 = std::tuple<uint32_t, uint32_t, uint32_t>;
using triplet_float = std::tuple<float, float, float>;
triplet_uint32 convert(triplet_float const & f) {
uint32_t precision = 1000000; // Allow for 6-digit precision.
uint32_t x = (uint32_t) (std::get<0>(f) * precision);
uint32_t y = (uint32_t) (std::get<1>(f) * precision);
uint32_t z = (uint32_t) (std::get<2>(f) * precision);
return {x, y, z};
}
int main() {
triplet_float pt1 = {1.f, 2.f, 3.f};
triplet_float pt2 = {1.0000001f, 2.0000001f, 3.0000001f}; // Considered duplicate with pt1.
triplet_float pt3 = {1.000001f, 2.000001f, 3.000001f}; // Considered NOT duplicate with pt1.
std::set<triplet_uint32> s;
s.insert(convert(pt1));
s.insert(convert(pt2));
s.insert(convert(pt3));
std::cout << "set size " << s.size() << std::endl;
for (auto const & t : s) std::cout << "set item " << std::get<0>(t) << ", " << std::get<1>(t) << ", " << std::get<2>(t) << ", " << std::endl;
}
>> g++ -std=c++20 -o set_uint32_triplet set_uint32_triplet.cpp
>> ./set_uint32_triplet
set size 2
set item 1000000, 2000000, 3000000,
set item 1000000, 2000001, 3000001,
Provavelmente esta não é a resposta que você deseja, mas oferece uma solução para o problema de transitividade da resposta aceita.
Use
std::tuple<int, int, int>
e quantize seus floats multiplicando-os por alguma constante (digamos 10.000) e arredondando-os para baixo. O fator de escala dependerá do seu domínio.Se você precisar manter o valor original, use um unordered_map. Use a tupla int para chave e adicione uma tupla flutuante para o valor. Então, ao pesquisar um determinado trigêmeo, você pode procurar a tupla adjacente em cada um dos três valores.
Isso é mais complexo e mais trabalhoso, mas seria uma abordagem mais correta e evitaria o UB devido a problemas de transitividade.
Problemas com KeyEqual
Em primeiro lugar, o KeyEqual fornecido para
std::unordered_set
não pode ser uma função, e é isso que você está tentando fazerdecltype(triplet_equal)
. No entanto, pode ser um ponteiro de função. Normalmente, você deve usar um objeto de função da seguinte maneira:Você não precisa fornecer nenhum valor para o hash ou para
triplet_equal
o construtor porque eles são construtíveis por padrão.Problemas com Hash
O próximo grande problema é que a biblioteca padrão não possui
std::hash
especialização para tuplas. Procure no hash genérico por tuplas em unordered_map / unordered_set se quiser fazer o seu próprio.No entanto, mesmo que existisse, permanece o problema de que duas tuplas iguais (onde a igualdade é determinada por
triplet_equal
) também devem ter o mesmo hash. Você teria questd::hash
se especializar para que duas tuplas iguais sempre tivessem o mesmo hash, apesar da imprecisão do ponto flutuante. Você pode conseguir fazer isso quantizandofloat
s, mas imagino que seria muito difícil fazer isso corretamente.Alternativa: use
std::set
e forneça um CompareSeria muito mais fácil de usar
std::set
, o que exige apenas que você implemente uma comparação menor que:Veja o exemplo ao vivo no Compiler Explorer
Notas Adicionais
Seria muito melhor não usar
std::tuple
, mas usar um tipo agregado simples como segue:Com operadores de comparação padrão, é muito fácil obter todas as funcionalidades do
std::tuple
, e você pode escreverlhs.x
em vez de precisarstd::get<0>(lhs)
de outros aborrecimentos.