【问题标题】:Does C++ string hashing hash the string or the memory address?C++ 字符串散列是否散列字符串或内存地址?
【发布时间】:2017-05-03 00:16:57
【问题描述】:

我以前从未研究过散列算法,我很惊讶在使用 std::unordered_map 时发现散列函数(我认为)实际上是散列内存地址,而不是字符串。如果我错了,请纠正我,但我只是通过更改原始字符串并将其添加到我的 unordered_map 中发现了这一点,并且当内存地址(指针)相同时,它从未添加任何内容。

并且在以下情况下是否添加新键取决于std::string是否重新分配到另一个内存区域:

std::unordered_map<const char*, char*> myMap;

std::string myString = "Key1";

myMap[myString.c_str()] = "someVal";    // <--- Adds a new key, size is now 1
myString = "Key2";
myMap[myString.c_str()] = "someVal";    // <--- Doesn't add a new key "Key2" didn't need to be reallocated

但是,当我更改字符串时直接在模板中使用 std::string 时,它确实向我的地图添加了另一个键,所以这表明 unordered_map 模板专门用于 std::string 并且实际上对字符串本身进行哈希处理?如果必须对字符串本身进行哈希处理,这种方式会不会更慢?

我提出这个问题的原因是,我看到的教程似乎传达了这样的意思,即被散列的实际字符串本身。即使在 Stack Overflow 上,我也看到人们出于性能原因说(解释)“不需要检查整个字符串,只需要检查尽可能多的字符”。

我得到的印象对于字符串文字和指向字符串的指针显然是错误的,但对于 std::string 类却不是?

【问题讨论】:

  • char* 不是字符串。

标签: c++ string dictionary hash std


【解决方案1】:

使用指针作为键可能是一种解决方案,但仅适用于 CONSTANT 字符串 - 指针是最简单和最快的哈希。您可以使用不同的 const 变量来初始化无序映射,确保它们的生命周期是合适的。

【讨论】:

    【解决方案2】:

    C++ 字符串散列是对字符串还是内存地址进行散列?

    这个问题实际上是关于 equalityidentity 的问题,并且取决于您说“字符串”时的意思。

    • 平等。如果您的意思是std::string 类,那么散列与内存地址无关。字符串的实际内容是散列的。两个std::string 实例相等,如果内容彼此相等,则生成相同的哈希。

    • Identity. 如果您的意思是指向内存中某些字符的指针,那么内存地址会被散列,而不管那里保存什么数据。两个“字符串”是相同的,如果它们指向相同的内存位置,它们会产生相同的哈希值。


    当您处理字符串时,您几乎总是希望进行相等性比较,因此我们鼓励您使用 std::string,因为即使数据位于不同的内存地址,std::string 始终为您提供这些语义,无论是散列还是简单的比较,如 myStr1 == myStr2[*]

    散列char const*char* 是非常危险的,因为你会遇到很多实现定义的行为。字符串文字是这方面的主要例子。例如,考虑以下程序:

    #include <iostream>
    
    int main()
    {
        char const *a = "foo";
        char const *b = "foo";
    
        std::cout << reinterpret_cast<void const*>(a) << "\n";
        std::cout << reinterpret_cast<void const*>(b) << "\n";
    }
    

    C++ 标准不会告诉您地址是否相同。编译器通常允许您控制这种行为。例如,Visual C++ 有 /GF 标志。如果你打开它,地址将是相同的;否则,他们不会。

    这对散列有相当严重的影响。在下面的程序中,实现定义是打印 1 还是 2:

    #include <iostream>
    #include <unordered_map>
    
    int main()
    {
        char const *a = "foo";
        char const *b = "foo";
    
        std::unordered_map<char const*, char*> myMap;
        myMap[a] = "1";
        myMap[b] = "2";
    
        std::cout << myMap.size() << "\n"; // prints 1 or 2
    }
    

    您的代码还具有实现定义的行为;不是因为文字,而是以不同的方式:

    并且在以下情况下是否添加新密钥取决于 std::string 是否重新分配到另一个内存区域:

    是的。您永远不应该从两个不同的 std::string 实例中获取 c_str() 指针,并假设指针相同只是因为 std::string 实例相等。

    如果它必须对字符串本身进行哈希处理,这种方式会更慢吗?

    不。我挑战你想出一个实际的用例,你可以实际衡量差异。只有这样,它才会“慢一些”。否则,这就是简单的过早优化。

    但还有更多。从技术上讲,散列单个地址应该比使用整个字符串内容(或大部分内容)来计算散列值更快,因为涉及更多数据。这很明显。但我不确定您是否看到执行“昂贵”计算的必要性。这没有魔法。如果您的程序逻辑关心字符串的内容,则必须考虑各个字符。即使在理论上,您应该如何对未读取的数据进行哈希处理?

    或者,更一般地说,如何散列你没有的东西?


    [*] 巧合的是,没有考虑到这种区别是 Java 中一个非常常见的错误的同一来源,即 str1 == str2str1.equals(str2) 具有不同的语义。

    【讨论】:

      【解决方案3】:

      如果您查看std::hash 的标准基本专业化。 const char * 没有特化,因为它只是一个指向字符数组的指针。但是,任何指针类型都有一个特化:

      template< class T > struct hash<T*>;
      

      这是std::unordered_map 使用的。它只是对地址进行哈希处理。


      简单地使用const char* 作为你的std::unordered_map 的键,default hashequality 是混乱的,因为默认的 hash 函数对地址进行哈希处理,而默认的相等性函数将比较地址。您应该首选 std::string 作为您的密钥,否则您需要执行以下操作:

      std::unordered_map<const char*, char*, MyCustomHash, MyCustomEquality> myMap;
      

      【讨论】:

      • 对不起,我不知道更多是我的错,但是比较地址不会是简单的 size_t 比较,而字符串比较会更慢吗?是不是每当你给map一个字符串键时,它需要比较多个字符才能跳转到正确的位置?
      • @TitoneMaurice 是的。但是 Andy 可能会给我一个存储在某处的字符串“Hello”,Bob 可能会给我另一个存储在其他地方的字符串“Hello”。如果我们只比较地址,我们将无法知道它们是否相同。除非我们深入研究字符串并一个接一个地比较它们。此外,使用std::string 可以提高性能,因为在逐字符比较之前,如果大小不同,我们可以得出比较结论。
      • 只是为了迂腐,地址表示为std::uintptr_t而不是std::size_t
      • @TitoneMaurice:标准类和函数通常以非常巧妙的方式进行优化,比典型的用户代码要优化得多。
      【解决方案4】:

      您错误地认为const char* 是一个字符串。它实际上是一个指针。因此std::unordered_map&lt;const char*, anything&gt; 使用指针(const char* 类型)作为键,std::hash 的特化指针(对地址进行散列)作为散列键。

      如果你想使用字符串作为键,你应该使用std::string,例如std::unordered_map&lt;std::string, anything&gt;.


      edit 我还应该说,使用指针代替字符串至少是危险的,但通常是不可能的。它不会像你想的那样做。问题是字符串(字符序列)和它的地址(指针)不一定在程序的生命周期内配对(尽管对于某些const char* 对象可能是这样)。想想以下

      std::unordered_map<const char*,int> map;
      char str[11] = "bad";
      map[str] = 2;           // hashes str = char*
      auto x = map["bad"];    // hashes address of "bad"; x!=2
      

      这说明使用地址作为键不能按预期工作:您无法从字符序列中获取元素 ("bad")

      【讨论】:

      • 是的,我明白了,它是一个指向字符串的“指针”。当您将它们传递到需要字符串的任何地方时,我已经习惯将 char* 视为字符串。所以 std::string 是唯一对实际字符串本身进行哈希处理的情况?我想在这种情况下,在地图中查找时,散列和查找都比较慢?想一想,如果是这样的话,是不是使用简单的指针和字符串文字作为关键优势?哦,我有很多问题。
      • char* 是指向单个字符的指针。有时它碰巧指向一个程序员将视为“字符串”的字符数组。 char* 没有附加这样的语义 - 所以所有代码都必须假设“char* 是指向单个字符的指针”,除非有其他明确指定(这不是映射的情况)。
      【解决方案5】:

      代码行为正确,因为密钥是const char*。 尝试使用std::string 作为获取您正在寻找的行为的关键。

      所以:std::unordered_map&lt;std::string, char*&gt; myMap;

      【讨论】:

        猜你喜欢
        • 2011-09-30
        • 2020-03-17
        • 1970-01-01
        • 1970-01-01
        • 2011-12-25
        • 1970-01-01
        • 1970-01-01
        • 2020-07-14
        • 2021-09-05
        相关资源
        最近更新 更多