为什么std::hash不是一个重载函数?

11

我猜测 std::hash 被定义为模板结构体是为了避免在重载函数解析期间进行的隐式类型转换。这样说是正确的吗?

我的意思是,我更喜欢这样写:

std::string s;
size_t hash = std::hash(s);

而不是

std::string s;
size_t hash = std::hash<std::string>()(s);

但我猜想标准委员会选择第二个选项肯定有原因。


@RiaD 好的,谢谢。突然间,感觉更加尴尬了。 - Alexei Sholik
2个回答

14

对于函数模板无法进行部分特化,因此对于自定义的带有模板的类,如果其为函数,则无法进行专门化。std::hash也无法进行专门化。 (您只能从std 命名空间中专门化模板,但不能重载,因此您自己使用的模板类的用户无法创建std :: unordered_map< MyClass <whatever>, MyOtherClass > ,他们将被迫选择 std :: unordered_map< MyClass<whatever>, MyOtherClass,???>)。 因此,在这里,函数对象是解决方案。

<code>namespace std
{
    template<typename T>
    struct hash<MyVector<T>>
    {
        size_t operator()(const MyVector<T>& v)
        {
            //return hash from here
        }
    };
}
</code>

标准库的另一种替代方法是使用SFINAE模板技巧来选择成员.hash()作为默认值,以及在其他情况下使用标准哈希函数,但在大多数情况下无法修改接口(特别是当使用第三方代码时)。

另一种替代方法类似于std::swap的做法(通过ADL技巧实现):

//somewhere in std::unordered_map
using std::hash;
size_t h = hash(key);
根据我的经验,ADL很棘手,而且不是每个人都能记得边角用例。此外,函数对象的优势在于可以将其用作模板参数,因此您可以为模板插入另一个函数对象(例如std::unordered_map<A,B,specialized_hash<A>>),如果您认为默认值不适合您的情况。
关于std::swapstd::hash之间有一点不同:
std::hash中:
- 可能会出现类作者定义的std::string哈希对于您的情况不够用的情况,也就是说它过于通用,因此您可以提供更快或/和具有更少碰撞的哈希函数。 - 有许多种不同目的的哈希,因此通用性在这里远不如重要。 - 在大多数情况下,您都可以创建更好的哈希。
std::swap中:
- 您不太可能需要自己的交换函数,但是您仍然可能希望使用特定于此类的交换函数,而不是调用复制构造函数的通用std::swap函数。 - 在大多数情况下,您甚至无法创建交换函数,因为它需要了解类内部的知识(例如,std::vector可以实现为隐藏为私有字段的指针的动态数组,因此您将无法访问它们,更不用说交换它们了,即使是以这种方式实现也不能保证)。 - 只有一个交换函数(或应该只有一个)。 - 实际上,std::swap存在问题:标准容器提供swap成员函数,std::swap可以被特化(但仅适用于非模板类),并且可以定义为通过ADL找到的自由函数。您该如何提供自己的交换?在我看来,这很令人困惑,而不是std::swap是函数而std::hash是函数对象的事实。
STL为什么不一致?我只能猜测,但STL不一致的主要原因是(a)向后兼容性和(b)C ++本身也相当不一致。

@milleniumbug:你有标准参考资料吗,说明不能重载在std命名空间中定义的函数吗?我本以为ADL会找到你的哈希函数。 - Andrew Tomazos
@user1131467 17.4.3.1/1 对于C++程序来说,在未经特别说明的情况下,向命名空间std或带有命名空间std的命名空间添加声明或定义是未定义的。程序可以向命名空间std添加任何标准库模板的模板特化。这样一个标准库的特化(完全或部分)会导致未定义的行为,除非声明依赖于具有外部链接的用户定义名称,并且除非模板特化符合原始模板的标准库要求。(摘自https://dev59.com/FHVD5IYBdhLWcg3wWKLc#109613) - milleniumbug
1
当然可以,但是如果您在命名空间中定义了hash(MyType),然后std::unordered_map<T>std命名空间内部调用了hash(x),那么由于ADL(这是其中的一个主要点),std::unordered_map<MyType>将使用您的哈希函数。问题是为什么标准库使用模板函数对象而不是刚才描述的重载函数。 - Andrew Tomazos
2
抱歉,请重试:该提案讨论了如何避免为哈希特化打开std命名空间以及如何使用ADL:[链接](http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2012/n3333.html) - Tony
@android 由于评论字段太短,我编辑了我的答案并将回复放在那里。 - milleniumbug
显示剩余3条评论

4

可能的一个原因是,这样做可以更容易地通过可变选项在模板中将其用作默认值。

template <typrname T, typename = std::hash<T>...>
class unordered_set;

顺便一提,您可以创建按此方式工作的函数。

template<typename T, typename... Args>
auto hasher(Args&&... args) -> whatever { 
    return std::hash<T>(std::forward<Args>(args)...)( //maybe some &'s  skipped
}

或者(为了允许检测类型)
template<typename T, typename... Args> 
auto hasher(T t) -> whatever {
    return std::hash<T>()(t);
}

struct std::hasher<T> { size_t operator()(T &&x) { return std::hash(std::forward<T>(x)) } };或类似的东西?相似地,相等性是用类似的方式实现的,operator==被重载,std::equal_to<T>(默认情况下)委托给operator== - user395760
可以实现,但我认为这并不是很重要,因为hash不是一个非常有用的功能(通常只有像集合这样的数据结构需要它(特别是在通用方式下)。如果它不是那么重要,为什么我们需要再增加一层间接性。至于C语言中的==:它是在std::equal_to之前引入的,并且equal_to是添加到已经存在的功能中的。 - RiaD
正如 OP 所示,当您不关心允许替换时,调用 hash 会变得更加简单。至于间接引用:您是否查看过标准库?;-) - user395760
可能他们根本没有考虑直接使用 :) - RiaD
@delnan,这可能是反过来的:) - RiaD

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接