是否可以在便携式C ++ 03代码中散列指针?

时间:2013-01-05 01:11:16

标签: c++ pointers hash language-lawyer

是否可以移植散列C ++ 03中的指针,该指针没有定义std::hash

在C ++中包含指针的hashables似乎很奇怪,但我想不出任何方法来制作它们。

我能想到的最接近的方式是reinterpret_cast<uintptr_t>(ptr),但{C} 03中不需要定义uintptr_t,我也不确定这个值是否可以合法操纵如果 定义了......这甚至可能吗?

2 个答案:

答案 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

但在此之下,xy 的值表示可以不同!

让我们玩编译器。我们将实现指针的值表示。假设我们需要(出于假设的架构原因)指针至少为两个字节,但只有一个用于该值。

我会跳过去说它可能是这样的:

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位。