【问题标题】:Is it possible to get hash values as compile-time constants?是否可以将哈希值作为编译时常量?
【发布时间】:2018-07-31 10:58:52
【问题描述】:

我想我会尝试通过散列选择不同的选项作为字符串,但这不起作用:

#include <type_traits>
#include <string>

inline void selectMenuOptionString(const std::string& str)
{
    switch (std::hash<std::string>()(str))
    {
    case std::hash<std::string>()(std::string("Selection one")) : break; 
        // Expression must have a constant value
    }
}

inline void selectMenuOptionString2(const std::string& str)
{
    size_t selectionOneHash = std::hash<std::string>()(std::string("Selection one"));

    switch (std::hash<std::string>()(str))
    {
    case selectionOneHash: // Expression must have a constant value 
                           // The variable of selectionOneHash cannot be used as a constant
    }

    constexpr size_t hash = std::hash<int>()(6); // Expression must have a constant value
}

似乎我无法在编译时获取哈希值。从我读过的内容来看,每个不同的输入每次都应该产生相同的独特输出,碰撞的可能性非常低。鉴于这些属性不能在编译时计算哈希值吗?我对哈希一无所知,我通常使用 unordered_map,但为了学习,我想尝试一些新的东西。

【问题讨论】:

  • std::hash 在运行时进行评估。它不能用于编译时散列。
  • A 哈希值原则上可以在编译时计算,但标准库中的 std::hash 当前未指定为 constexpr,因此不能。此外,您目前无法创建 std::string 常量表达式(因为还没有 constexpr operator new)。面向未来的最佳选择可能是考虑std::hash&lt;std::string_view&gt;,但我们还没有这个constexpr。
  • 你当然可以复制 std::hash&lt;std::string&gt;::operator() 的实现并将其粘贴到 constexpr 函数中。
  • 相关:Compile time string hashing,但这是针对 C++11 的;使用 C++14 或 C++17,我们可以做得更好。
  • @Kerrek 这很有趣,我一直在看,它基本上是在循环中为数组重复的两行:_Val ^= (size_t)_First[_Next];和 _Val *= _FNV_prime;

标签: c++ hash compile-time-constant


【解决方案1】:

std::hash::operator() 不是constexpr,所以你不能只使用它。相反,您必须编写自己的 constexpr 哈希函数。比如下面是FNV-1a hash algorithm(未测试):

template <typename Str>
constexpr size_t hashString(const Str& toHash)
{
    // For this example, I'm requiring size_t to be 64-bit, but you could
    // easily change the offset and prime used to the appropriate ones
    // based on sizeof(size_t).
    static_assert(sizeof(size_t) == 8);
    // FNV-1a 64 bit algorithm
    size_t result = 0xcbf29ce484222325; // FNV offset basis

    for (char c : toHash) {
        result ^= c;
        result *= 1099511628211; // FNV prime
    }

    return result;
}

然后你就可以使用它了:

int selectMenuOptionString(const std::string& str)
{
    switch (hashString(str))
    {
    case hashString(std::string_view("Selection one")): return 42; 
    default: return 0;
    }
}

请注意,如果您编写了hashString("Selection one"),它实际上也会散列空终止符,因此您可能希望有一个重载来捕获字符串文字,例如:

template <size_t N>
constexpr size_t hashString(char const (&toHash)[N])
{
    return hashString(std::string_view(toHash));
}

Demo

【讨论】:

  • 很好 - 我有点落后于时代,尤其是在constexpr
  • 在哈希函数中看到它一直迭代到最后一个字符,并且它与 size_t 进行异或,我认为它会溢出缓冲区,但相反,它只异或 8 位,它降级它,可以这么说。那么这是否使结果只包含最低有效八位而其余为空?
  • @Zebrafish 你可以try it。显然,设置了更多位。而且,promotion rules 意味着 char 将被提升为用于 XOR 的 size_t
【解决方案2】:

您需要实现自己的哈希函数,因为没有合适的 std::hash 实例化即 constexpr。这是一个便宜又脏的...

编辑:为了不被贾斯汀的回答蒙羞,我添加了一个 32 位分支。

    constexpr size_t hash(const char *str) {
    static_assert(sizeof(size_t) == 8 || sizeof(size_t) == 4);
    size_t h = 0;
    if constexpr(sizeof(size_t) == 8) {
        h = 1125899906842597L; // prime
    } else {
        h = 4294967291L;
    }
    int i = 0;
    while (str[i] != 0) {
        h = 31 * h + str[i++];
    }

    return h;
}

【讨论】:

  • 我是哈希函数的新手,但该死的,这似乎是一堆随机步骤。哈哈。不过,我注意到素数是一个共同的主题。
【解决方案3】:

您无法在编译时获取运行时值的哈希值,不。

即使您传递了std::hash 一个常量表达式,它也没有被定义为能够在编译时进行散列工作。

据我所知(不远),你必须想出一些可怕的模板元黑客(或者,更糟糕的是,宏!)来做到这一点。就个人而言,如果您的文本输入在构建时是已知的,我会在代码之外预先生成一个哈希,也许在一些 Python 驱动的预构建步骤中。

【讨论】:

  • 我喜欢 Kerrek 将实现复制到 constexpr 函数中的想法。我查了一下,它在公共领域,基本上是两行,非常好。 Microsoft 基本上使用了 FNV 哈希函数的 Wikipedia 文章中的确切数字的确切实现。
  • 从 C++14 开始,你不需要任何可怕的模板元黑客;你可以只使用constexpr 函数(好吧,你也可以在 C++11 中使用,但你必须使用递归,这使得它更难理解)
【解决方案4】:

我只是想添加这个,因为我觉得它很酷。我从这里的一个问题中得到的 constexpr strlen:constexpr strlen

#include <iostream>
#include <string>

int constexpr strlength(const char* str)
{
    return *str ? 1 + strlength(str + 1) : 0;
}

size_t constexpr Hash(const char *first)
{   // FNV-1a hash function 
    const size_t FNVoffsetBasis = 14695981039346656037ULL;
    const size_t FNVprime = 1099511628211ULL;
    const size_t count = strlength(first);
    size_t val = FNVoffsetBasis;
    for (size_t next = 0; next < count; ++next)
    {
        val ^= (size_t)first[next];
        val *= FNVprime;
    }
    return val;
}

inline void selectMenuOptionString(const std::string& str)
{
    switch (Hash(str.c_str()))
    {
    case Hash("Selection one"): /*Do something*/ break;
    case Hash("Selection two"): /*Do something*/ break;
    }
}

int main()
{
    static_assert(strlength("Hello") == 5, "String length not equal");
}

【讨论】:

  • 在 C++14 之后,你可以更简单地编写 constexprstrlen;您不必使用递归。在 C++17 之后,您也可以将 std::char_traits&lt;char&gt;::length(str) 写为您的 constexpr strlen
  • 你也可以做一个constexpr strlen,它对字符串文字没有任何作用,使用模板:template &lt;size_t N&gt; constexpr size_t strlength(char const (&amp;)[N]) { return N - 1; }- 1是忽略空终止符)
  • @Justin 最后一个解决方案非常棒,但我不明白。这个论点究竟是什么?对 const char 数组的引用对吗?如果没有引用,它将衰减为指针,您将无法获得数组大小,对吗?为什么&周围的括号?是因为没有括号它被解释为数组或引用吗?这个语法业务真的很难。
  • 完全正确。这是丑陋的东西,所以我总是去std::end #2看看当我忘记它时语法是什么
  • @Justin 为什么不接受 strlength(const char arg[N])?我传入 strlength("hel");并且编译器说: strlength(const char [N])': 无法从'const char [4]' 中推断出'const char [N]' 的模板参数。所以它知道这是一个 [4] 的 char 数组,而不是指针。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-23
  • 1970-01-01
  • 1970-01-01
  • 2013-06-23
  • 2010-12-01
相关资源
最近更新 更多