是否可以std::hash
?
在C ++中包含指针的hashables似乎很奇怪,但我想不出任何方法来制作它们。
我能想到的最接近的方式是reinterpret_cast<uintptr_t>(ptr)
,但{C} 03中不需要定义uintptr_t
,我也不确定这个值是否可以合法操纵如果 定义了......这甚至可能吗?
答案 0 :(得分:9)
不,一般。实际上,在没有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;
}
因为指针的值实际上只包含在第一个字节中(即使它的表示有两个字节),这就是我们必须比较的全部内容。第二个字节无关紧要,即使它们不同。
我们需要address-of运算符实现,当然:
__pointer_impl address_of(int& i)
{
__pointer_impl result;
result.byte1 = /* hypothetical architecture magic */;
return result;
}
这个特定的实现重载为我们提供了给定int
的指针值表示。请注意,第二个字节未初始化!没关系:它对于值并不重要。
这就是我们所需要的全部内容。假装其余的实现已完成。 :)
现在再考虑我们的第一个例子,“编译器化”:
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
会给你相同的结果。
所以第一个问题是:在这个实现上,对于相等的指针值,我们的哈希可能会有所不同。
这个想法已经出来了。也许我们可以进入表示本身并将字节散列在一起。但这显然会导致同样的问题,这就是你的问题的评论所暗示的。那些讨厌的未使用的表示位总是在路上,并且没有办法弄清楚它们在哪里,所以我们可以忽略它们。
我们被困住了!这是不可能的。 一般而言。
请记住,在实践中我们会针对某些实现进行编译,并且因为这些操作的结果是实现定义的,所以如果您只是正确使用它们,那么它们是可靠的。这就是Mats Petersson is saying:找出实施的保证,你会没事的。
事实上,您使用的大多数消费者平台都会处理std::uintptr_t
尝试。如果它在您的系统上不可用,或者您想要一种替代方法,只需组合指针中各个字节的哈希值即可。所有这些需要工作的是未使用的表示位总是采用相同的值。实际上,这是MSVC2012使用的方法!
如果我们假设的指针实现总是将byte2
初始化为常量,那么它也可以在那里工作。但是实现这一点并没有任何要求。
希望这能澄清一些事情。
答案 1 :(得分:5)
你的问题的答案真的取决于你想要的“HOW portable”。许多架构都有一个uintptr_t,但是如果你想要可以在DSP,Linux,Windows,AIX,旧Cray机器,IBM 390系列机器等上编译的东西,那么你可能必须有一个配置选项来定义你的如果在该架构中不存在,则拥有“uintptr_t”。
将指针转换为整数类型应该没问题。如果你要把它丢回来,你可能会遇到麻烦。当然,如果您有许多指针,并且在64位计算机上使用32位整数分配相当大的内存部分,则有可能会发生大量的冲突。请注意,64位窗口的“长”仍为32位。