默认情况下,使 std 的数据结构使用我现有的非静态哈希函数"hashCode()"

Make std's data-structure use my existing non-static hash function "hashCode()" by default

本文关键字:函数 哈希 静态 hashCode std 情况下 数据结构 默认      更新时间:2023-10-16

我有一个中等大小的代码库(>200.cpp),它使用函数hashCode()返回哈希号:-

class B01{  //a class
//..... complex thing ....
public: size_t hashCode(){ /* hash algorithm #H01 */}  
};
class B02{  //just another unrelated class
//..... complex thing ....
public: size_t hashCode(){/* #H02 */}  //This is the same name as above
};

我已经在不同位置使用它,例如在我的自定义数据结构中。 效果很好。

现在,我想使哈希算法被std::数据结构识别:-

这是我应该做的:- (从 cpp 首选项修改,我将这段代码称为#D)。

//#D
namespace std {
template<> struct hash<B01> {
std::size_t operator()(const B01& b) const {
/* hash algorithm #H01 */
}
};
}

如果我在每个类(B01B02,...)中插入块#D(具有适当的实现),我可以调用:-

std::unordered_set<B01> b01s;
std::unordered_set<B02> b02s;

传递第二个模板参数,
我的哈希算法(#H01)将被调用。(默认)

问题

要使其识别我的所有B01::hashCode, B02::hashCode, ...
我是否必须将块#D插入所有 200+Bxx.h

我可以只添加一个块#D(在顶部标题中)吗?
并且,从那里,尽可能重新路由std::anyDataStructure呼叫hashCode()

//pseudo code
namespace std{
template<> struct hash<X>   {
std::size_t operator()(const X& x) const { // std::enable_if??
if(X has hashCode()){    //e.g. T=B01 or B02       
make this template highest priority   //how?
return hashCode();
}else{                   //e.g. T=std::string
don't match this template;  
}
}
};
}

对我来说,这听起来像是一个SFINAE问题。

旁注:SO中最相似的问题没有询问如何实现这一目标。

编辑(我为什么不重构它?;2017年2月3日)

  • 我不知道蛮力重构是否是正确的道路。 我想可能有更好的方法。
  • hashCode()是我的家。 我在情感上依恋它。
  • 我想让我的代码尽可能简短和干净。std::块很脏。
  • 可能只是我的好奇心。 如果我固执地不重构我的代码,C++能走多远?

不必如此,您也可以有一个函子:

struct MyHash {
template <class T>
auto hashCode(const T & t, int) const -> decltype(t.hashCode()) {
return t.hashCode();
}
template <class T>
auto hashCode(const T & t, long) const -> decltype(std::hash<T>{}(t)) {
return std::hash<T>{}(t);
}

template <class T>
auto operator()(const T & t) const -> decltype(hashCode(t,42)) {
return hashCode(t,42);
}
};

并有一个别名std::unordered_setMyHash作为哈希类型:

template <class Key>
using my_unordered_set = std::unordered_set<Key, MyHash>;

或者更完整,如果您还希望能够提供相等函子和分配器:

template<
class Key,
class KeyEqual = std::equal_to<Key>,
class Allocator = std::allocator<Key>
>
using my_unordered_set = std::unordered_set<Key, MyHash, KeyEqual, Allocator>;

然后使用它(与您的任何 Bxx)就像您使用std::unordered_set一样:

int main() {
my_unordered_set<B01> b01s;
my_unordered_set<B02> b02s;
// or lonely with your type:
B01 b01{/*...*/};
std::cout << MyHash{}(b01) << std::endl;
// or any other:
std::string str{"Hello World!"};
std::cout << MyHash{}(str) << std::endl;
}

概念

如果您可以使用概念,它们可以让您按照所需的方式专门化std::hash类:

template <class T>
concept HashCodeConcept = requires(T const & t)
{
{t.hashCode()} -> std::same_as<std::size_t>;
};
namespace std {
template <HashCodeConcept T>
struct hash<T> {
std::size_t operator()(const T& t) const {
return  t.hashCode();
}
};
}

在创建条件以将 std 容器模板的哈希参数默认为类组的成员方法时,应避免引入新问题。

  • 冗余
  • 可移植性问题
  • 奥术结构

经典的面向对象方法可能需要对 200+ 类进行模式化编辑,以确保它们提供 std::hash 容器使用的基础知识。 下面给出了一些组转换选项,以提供两种所需的方法。

  • 公共 hashCode() 在具体类中定义,在该类中它是唯一的,或者如果它遵循跨类通用的模式,则通过继承定义。
  • 定义了公共运算符==()。

两个模板

这两个模板将删除冗余并简化声明,如所示。

template <typename T>
struct HashStruct {
std::size_t operator()(const T & t) const {
return t.hashCode();
} };
template <class T>
using SetOfB = std::unordered_set<T, HashStruct<T>>;

节省积分时间

一个示例超类:

class AbstractB {
...
virtual std::size_t hashCode() const {
return std::hash<std::string>{}(ms1)
^ std::hash<std::string>{}(ms2);
} }

以下 sed 表达式可以节省转换时间,假设代码使用 { 内联。 类似的表达式可以与Boost一起使用,也可以使用Python等脚本语言。

"s/^([ t]*class +B[a-zA-Z0-9]+ *)(:?)(.*)$"
+ "/1 2 : public AbstractB, 3 [{]/"
+ "; s/ {2,}/ /g"
+ "; s/: ?:/:/g"

基于 AST 的工具会更可靠。 这说明了如何使用 clang 功能进行代码转换。 还有一些新的添加功能,例如C++代码转换的 Python 控制器。

讨论

哈希算法可以驻留的位置有几个选项。

  • std 容器声明的抽象类的方法
  • 具体类的方法(如示例中的 #H01)
  • 结构模板(通常适得其反且不透明)
  • 默认标准::哈希

这是一个编译单元,它清晰地演示了如何实现所需的默认值和上面列出的其他三个目标的经典内容,同时为任何给定类定义哈希算法的位置提供了灵活性。 根据具体情况,可以删除各种功能。

#include <string>
#include <functional>
#include <unordered_set>
template <typename T>
struct HashStructForPtrs {
std::size_t operator()(const T tp) const {
return tp->hashCode(); } };
template <class T>
using SetOfBPtrs = std::unordered_set<T, HashStructForPtrs<T>>;
template <typename T>
struct HashStruct {
std::size_t operator()(const T & t) const {
return t.hashCode(); } };
template <class T>
using SetOfB = std::unordered_set<T, HashStruct<T>>;
class AbstractB {
protected:
std::string ms;
public:
virtual std::size_t hashCode() const {
return std::hash<std::string>{}(ms); }
// other option: virtual std::size_t hashCode() const = 0;
bool operator==(const AbstractB & b) const {
return ms == b.ms; } };
class B01 : public AbstractB {
public:
std::size_t hashCode() const {
return std::hash<std::string>{}(ms) ^ 1; } };
class B02 : public AbstractB {
public:
std::size_t hashCode() const {
return std::hash<std::string>{}(ms) ^ 2; } };
int main(int iArgs, char * args[]) {
SetOfBPtrs<AbstractB *> setOfBPointers;
setOfBPointers.insert(new B01());
setOfBPointers.insert(new B02());
SetOfB<B01> setOfB01;
setOfB01.insert(B01());
SetOfB<B02> setOfB02;
setOfB02.insert(B02());
return 0; };

您正在寻找的基于SFINAE的方法需要对std::hash进行部分专业化。如果您的类Bxx是模板(如果它们派生自 CRTP 基库,则会出现这种情况),则可以执行此操作。例如(注释在编辑中充实)

#include <type_traits>
#include <unordered_set>
#include <iostream>
template<typename T = void>
struct B {
B(int i) : x(i) {}
std::size_t hashCode() const
{
std::cout<<"B::hashCode(): return "<<x<<std::endl;
return x;
}
bool operator==(B const&b) const
{ return x==b.x; }
private:
int x;
};
template<typename T,
typename = decltype(std::declval<T>().hashCode())> 
using enable_if_has_hashCode = T;
namespace std {
template<template<typename...> class T, typename... As> 
struct hash<enable_if_has_hashCode<T<As...>>> 
{
std::size_t operator()(const T<As...>& x) const
{ return x.hashCode(); }
};
// the following would not work, as its not a partial specialisation
//    (some compilers allow it, but clang correctly rejects it)
// tempate<typename T>
// struct hash<enable_if_hashCode<T>>
// { /* ... */ }; 
}
int main()
{
using B00 = B<void>;
B00 b(42);
std::unordered_set<B00> set;
set.insert(b);
}

产生(在 MacOS 上使用 clang++)

B::哈希值(): 返回 42

另请参阅对我的类似问题的相关回答。

然而,概念是解决此类问题的未来方式。

我想出了一些似乎部分工作的东西。这是一种解决方法,允许您在实现hashCode的类型上使用std::hash。看一看:

//some class that implements hashCode
struct test
{
std::size_t hashCode() const
{
return 0;//insert your has routine
}
};
//helper class
struct hashable
{
hashable():value(0){}
template<typename T>
hashable(const T& t):value(t.hashCode())
{}
template<typename T>
std::size_t operator()(const T& t) const
{
return t.hashCode();
}
std::size_t value;
};

//hash specialization of hashable
namespace std {
template<>
struct hash<hashable>
{
typedef hashable argument_type;
typedef std::size_t result_type;
result_type operator()(const argument_type& b) const {
return b.value;
}
};
}
//helper alias so you dont have to specify the hash each time.
template<typename T, typename hash = hashable>
using unordered_set = std::unordered_set<T,hash>;
int main(int argc, char** argv)
{
unordered_set<test> s;
test t;
std::cout<<std::hash<hashable>{}(t)<<std::endl;
}

该代码利用hashable的模板构造函数和模板运算符从实现hashCode的任何类中检索哈希。std::hash专用化是查找hashable的实例,但模板化构造函数允许从具有hasCode的类构造实例。

这里唯一的问题是你必须编写unordered_set而不是std::unordered_set使用它,并且你必须确保std::unordered_set不会以任何方式进入范围。因此,您将无法在源中具有类似using namespace stdusing std::unordered_set的内容。但是除了用法中的少数陷阱之外,这可能对您有用。

当然,这只是真正问题的创可贴......这将不想经历为您的每种类型适当专业化std::hash的痛苦。(我不怪你)

我还想指出,使用此代码替换是一个错误......如果你更喜欢SFINAE,则需要修改。

编辑:

尝试运行后:

unordered_set<test> s;
test t;
s.insert(t);

我注意到有一些编译器错误。

我已通过添加以下内容将我的test类更新为equality comparable

bool operator==(const test& other) const
{
return hashCode() == other.hashCode();
}

test现在使:

//some class that implements hashCode
struct test
{
std::size_t hashCode() const
{
return 0;//insert your has routine
}
bool operator==(const test& other) const
{
return hashCode() == other.hashCode();
}
};

解决方案一

如果你能把类B01B02、...具有虚拟参数的类模板,您可以简单地配合采用虚拟模板参数的模板模板std::hash的专用化:

#include <iostream>
#include <unordered_set>
struct Dummy {};
template <class = Dummy>
class B01{ 
public: size_t hashCode() const { return 0; }  
};
template <class = Dummy>
class B02{ 
public: size_t hashCode() const { return 0; } 
};
namespace std{
template<template <class> class TT> struct hash<TT<Dummy>>   {
std::size_t operator()(const TT<Dummy>& x) const { 
return x.hashCode();
}
};
}
int main() {
std::unordered_set<B01<>> us;
(void)us;
}

[现场演示]

解决方案二(包含错误/不要使用它)

但我相信你想要的看起来更像这样:

#include <iostream>
#include <unordered_set>
class B01{ 
public: size_t hashCode() const { return 0; }  
};
class B02{ 
public: size_t hashCode() const { return 0; } 
};
template <class T, class>
using enable_hash = T;
namespace std{
template<class T> struct hash<enable_hash<T, decltype(std::declval<T>().hashCode())>>   {
std::size_t operator()(const T& x) const { 
return x.hashCode();
}
};
}
int main() {
std::unordered_set<B01> us;
(void)us;
}

[现场演示]

(灵感来自这个答案)

但是,只要这可以在 gcc 上运行,它就不是 c++ 标准真正允许的(但我也不确定它是否真的是字面上不允许的......在此上下文中查看此线程。

编辑:

正如@Barry所指出的,这种 gcc 行为不是由 c++ 标准强制要求的,因此绝对不能保证即使在下一个 gcc 版本中也能正常工作......它甚至可以被视为一个错误,因为它允许对实际上并不专门化该模板的模板进行部分专用化。

解决方案三(首选)

另一种方法是专门化std::unordered_set而不是std::hash

#include <iostream>
#include <type_traits>
#include <unordered_set>
class specializeUnorderedSet { };
class B01: public specializeUnorderedSet { 
public: size_t hashCode() const { return 0; }  
};
class B02: public specializeUnorderedSet { 
public: size_t hashCode() const { return 0; } 
};
template <class T>
struct my_hash {
std::size_t operator()(const T& x) const { 
return x.hashCode();
}
};
template <class...>
using voider = void;
template <class T, class = void>
struct hashCodeTrait: std::false_type { };
template <class T>
struct hashCodeTrait<T, voider<decltype(std::declval<T>().hashCode())>>: std::true_type { };
namespace std{
template <class T>
struct unordered_set<T, typename std::enable_if<hashCodeTrait<T>::value && std::is_base_of<specializeUnorderedSet, T>::value, std::hash<T>>::type, std::equal_to<T>, std::allocator<T>>:
unordered_set<T, my_hash<T>, std::equal_to<T>, std::allocator<T>> { };
}
int main() {
std::unordered_set<B01> us;
(void)us;
}

根据这里提出的讨论,它应该是完全有效的。它也适用于gcc,clang,icc,VS

为了能够在不干扰类代码的情况下使用代码,我相信我们可以利用 ADL 规则来检查给定的类是否不涉及 std 命名空间。你可以在这里找到背景。也归功于干杯和hth。- 阿尔夫.该方法可以更改如下:

#include <utility>
#include <unordered_set>
#include <string>
#include <type_traits>
#include <functional>
template< class Type >
void ref( Type&& ) {}
template< class Type >
constexpr auto involve_std()
-> bool
{
using std::is_same;
using std::declval;
return not is_same< void, decltype( ref( declval<Type &>() ) )>::value;
}
class B01 { 
public: size_t hashCode() const { return 0; }  
};
class B02 { 
public: size_t hashCode() const { return 0; } 
};
template <class T>
struct my_hash {
std::size_t operator()(const T& x) const { 
return x.hashCode();
}
};
template <class...>
struct voider {
using type = void;
};
template <class T, class = void>
struct hashCodeTrait: std::false_type { };
template <class T>
struct hashCodeTrait<T, typename voider<decltype(std::declval<T>().hashCode())>::type>: std::true_type { };
namespace std{
template <class T>
struct unordered_set<T, typename std::enable_if<hashCodeTrait<T>::value && !involve_std<T>(), std::hash<T>>::type, std::equal_to<T>, std::allocator<T>>:
unordered_set<T, my_hash<T>, std::equal_to<T>, std::allocator<T>> { };
}
int main() {
std::unordered_set<B01> usb01;
std::unordered_set<std::string> uss;
static_assert(std::is_base_of<std::unordered_set<B01, my_hash<B01>>, std::unordered_set<B01>>::value, "!");
static_assert(!std::is_base_of<std::unordered_set<std::string, my_hash<std::string>>, std::unordered_set<std::string>>::value, "!");
(void)usb01;
(void)uss;
}

[GCC 测试], [CLANG 测试], [ICC 测试] [GCC 4.9] [VC]