【问题标题】:Is it possible to hash pointers in portable C++03 code?是否可以在可移植 C++03 代码中散列指针?
【发布时间】:2013-01-05 01:11:15
【问题描述】:

是否可以可移植地在 C++03 中散列一个未定义 std::hash 的指针?

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

我能想到的最接近的方法是做reinterpret_cast<uintptr_t>(ptr),但是uintptr_t不需要在C++03中定义,我不确定这个值是否可以合法操作,即使它定义...这可能吗?

【问题讨论】:

  • 我猜您正在寻找的不仅仅是执行 sizeof(ptr) 并将其视为字符序列?
  • @GuySirton:你的意思是unsigned char的序列?我不确定,将指针作为整数读取是否合法?
  • @Mehrdad:任何对象都可以读取为unsigned char的序列。但是,某些位可能没有定义的值。例如,如果您从 struct { int x : 3; } 读取字节。
  • 如果您只是读取它来散列值,您认为它是非法的吗?当然,如果您将其读取为 int,然后尝试以类似指针的方式使用它,这很糟糕,但我看不出您在这里会担心什么。
  • Dietrich 提出了一个有趣的观点。某些系统可以忽略指针的某些位(例如,128K Mac CPU 忽略了最高字节,因此操作系统将分配标志存储在那里)。如果您希望散列算法将指针视为相同,尽管内部有不同的位,您需要做一些特殊的事情来处理它。

标签: c++ pointers hash language-lawyer


【解决方案1】:

不,一般来说。事实上,如果没有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;
}

因为指针的值实际上只包含在第一个字节中(即使它的表示有两个字节),这就是我们要比较的全部内容。第二个字节无关紧要,即使它们不同

当然,我们需要地址操作符实现:

__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 初始化为一个常量,它也可以在那里工作。但对实现没有任何要求。

希望这能澄清一些事情。

【讨论】:

  • 真棒,清晰的答案!除了感谢一堆,没什么可说的! :)
  • 您的分析就标准所保证的内容而言是完全正确的,但请考虑一下可怜的 schmuck 所经历的快乐或缺乏,他们必须在独立的实现之上实现 malloc正如您所描述的(出于此假设的目的,假设他们没有从编译器获得任何其他帮助)。换句话说,进行指针到整数转换的编译器团队会表现得像这样,IMNSHO 很快就会在他们的大楼外找到一群愤怒的操作系统程序员。
  • 有“允许编译器做”,也有“编译器做”。因为 C++ 标准中有很多“漏洞”,允许出现非常荒谬的行为。您能否提供一个编译器示例,该编译器生成整数,其uint_ptr 表示对于比较相等的有效派生指针不同?如果没有人或某些人拥有该属性,我不会感到惊讶,但知道很重要。
  • @Yakk:没有线索。这个问题明确需要纯 C++ 解决方案,这在技术上意味着您必须假设如果某个编译器可以做一些事情来搞砸它,那么一个人会这样做。在实践中,不知道;问题上的 cmets 似乎表明存在/曾经有这样的平台,但这些编译器可能不再有用了。 :)
【解决方案2】:

您的问题的答案实际上取决于您想要它的“便携性”。许多架构都会有一个 uintptr_t,但是如果你想要一些可以在 DSP、Linux、Windows、AIX、旧 Cray 机器、IBM 390 系列机器等上编译的东西,那么你可能需要一个配置选项来定义你的如果该架构中不存在“uintptr_t”,则拥有它。

将指针转换为整数类型应该没问题。如果你把它扔回去,你可能会遇到麻烦。当然,如果您有很多指针,并且您在 64 位机器上分配了相当大的内存部分,使用 32 位整数,那么您可能会遇到很多冲突。请注意,64 位窗口仍然有一个“长”作为 32 位。

【讨论】:

    猜你喜欢
    • 2023-04-02
    • 2017-03-11
    • 2013-10-13
    • 2021-12-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多