不,一般来说。事实上,如果没有std::hash,在 C++11 中甚至是不可能的。
原因在于值和值表示的区别。
您可能还记得用于演示值与其表示之间的差异的非常常见的示例:空指针值。许多人错误地认为这个值的表示都是零位。这不能以任何方式保证。仅通过其价值来保证您的行为。
再举一个例子,考虑:
int i;
int* x = &i;
int* y = &i;
x == y; // this is true; the two pointer values are equal
尽管如此,x 和 y 的值表示可能不同!
让我们玩编译器。我们将实现指针的值表示。假设我们需要(出于假设的架构原因)指针至少为两个字节,但只有一个用于值。
我会直接说它可能是这样的:
struct __pointer_impl
{
std::uint8_t byte1; // contains the address we're holding
std::uint8_t byte2; // needed for architecture reasons, unused
// (assume no padding; we are the compiler, after all)
};
好的,这是我们的值表示,现在让我们实现值语义。一、平等:
bool operator==(const __pointer_impl& first, const __pointer_impl& second)
{
return first.byte1 == second.byte1;
}
因为指针的值实际上只包含在第一个字节中(即使它的表示有两个字节),这就是我们要比较的全部内容。第二个字节无关紧要,即使它们不同。
当然,我们需要地址操作符实现:
__pointer_impl address_of(int& i)
{
__pointer_impl result;
result.byte1 = /* hypothetical architecture magic */;
return result;
}
这个特定的实现重载为我们提供了给定int 的指针值表示。请注意,第二个字节未初始化!没关系:这对 value 来说并不重要。
这就是我们真正需要的全部内容。假装其余的实现已经完成。 :)
所以现在再次考虑我们的第一个示例,“编译器化”:
int i;
/* int* x = &i; */
__pointer_impl x = __address_of(i);
/* int* y = &i; */
__pointer_impl y = __address_of(i);
x == y; // this is true; the two pointer values are equal
对于我们关于假设架构的小例子,这充分提供了指针值标准所要求的保证。但请注意,您永远无法保证 x == y 暗示 memcmp(&x, &y, sizeof(__pointer_impl)) == 0。根本没有对值表示的要求。
现在考虑您的问题:我们如何散列指针?也就是我们要实现:
template <typename T>
struct myhash;
template <typename T>
struct myhash<T*> :
std::unary_function<T*, std::size_t>
{
std::size_t operator()(T* const ptr) const
{
return /* ??? */;
}
};
最重要的要求是如果x == y,那么myhash()(x) == myhash()(y)。我们也已经知道如何散列整数。我们能做什么?
唯一我们能做的就是尝试以某种方式将指针转换为整数。好吧,C++11 给了我们std::uintptr_t,所以我们可以这样做,对吧?
return myhash<std::uintptr_t>()(reinterpret_cast<std::uintptr_t>(ptr));
也许令人惊讶的是,这是不正确的。要了解原因,请再次想象我们正在实施它:
// okay because we assumed no padding:
typedef std::uint16_t __uintptr_t; // will be used for std::uintptr_t implementation
__uintptr_t __to_integer(const __pointer_impl& ptr)
{
__uintptr_t result;
std::memcpy(&result, &ptr, sizeof(__uintptr_t));
return result;
}
__pointer_impl __from_integer(const __uintptr_t& ptrint)
{
__pointer_impl result;
std::memcpy(&result, &ptrint, sizeof(__pointer_impl));
return result;
}
所以当我们reinterpret_cast 指向整数的指针时,我们将使用__to_integer,然后我们将使用__from_integer。请注意,生成的整数将具有取决于指针值表示中的位的值。也就是说,两个相等的指针值可能以不同的整数表示形式结束……这是允许的!
这是允许的,因为reinterpret_cast 的结果完全是实现定义的;你只能保证相反的reinterpret_cast 的结果会给你同样的结果。
所以有第一个问题:在 this 实现中,对于相同的指针值,我们的哈希最终可能会有所不同。
这个想法已经过时了。也许我们可以深入到表示本身并将字节散列在一起。但这显然会以同样的问题告终,这就是您问题中的 cmets 所暗示的。那些讨厌的未使用的表示位总是在路上,我们无法弄清楚它们在哪里,所以我们可以忽略它们。
我们被困住了!这是不可能的。 一般。
请记住,实际上我们为某些实现进行编译,并且由于这些操作的结果是实现定义的,因此如果您注意仅正确使用它们,它们是可靠的。这就是Mats Petersson is saying:找出实现的保证,你会没事的。
事实上,您使用的大多数消费者平台都可以很好地处理std::uintptr_t 尝试。如果它在您的系统上不可用,或者如果您想要一种替代方法,只需组合指针中各个字节的哈希值。所有这一切都需要工作是未使用的表示位始终采用相同的值。其实这就是MSVC2012使用的方法!
如果我们假设的指针实现总是简单地将byte2 初始化为一个常量,它也可以在那里工作。但对实现没有任何要求。
希望这能澄清一些事情。