在 C++ 中,为什么要在 `const char array` 上重载函数和包装 `const char*` 的私有结构?

In C++, why overload a function on a `const char array` and a private struct wrapping a `const char*`?

我最近 运行 进入了 ENTT 库中一个令人着迷的 class。 class 用于计算字符串的哈希值,如下所示:

std::uint32_t hashVal = hashed_string::to_value("ABC");

hashed_string hs{"ABC"};
std::uint32_t hashVal2 = hs.value();

在查看此 class 的实现时,我注意到构造函数或 hashed_string::to_value 成员函数的 none 直接采用 const char*。相反,他们采用一个名为 const_wrapper 的简单结构。下面是 class' 实现的简化视图来说明这一点:

/*
   A hashed string is a compile-time tool that allows users to use
   human-readable identifers in the codebase while using their numeric
   counterparts at runtime
*/
class hashed_string
{
private:

    struct const_wrapper
    {
        // non-explicit constructor on purpose
        constexpr const_wrapper(const char *curr) noexcept: str{curr} {}
        const char *str;
    };

    inline static constexpr std::uint32_t calculateHash(const char* curr) noexcept
    {
        // ...
    }

public:

    /*
       Returns directly the numeric representation of a string.
       Forcing template resolution avoids implicit conversions. An
       human-readable identifier can be anything but a plain, old bunch of
       characters.
       Example of use:
       const auto value = hashed_string::to_value("my.png");
    */
    template<std::size_t N>
    inline static constexpr std::uint32_t to_value(const char (&str)[N]) noexcept
    {
        return calculateHash(str);
    }

    /*
       Returns directly the numeric representation of a string.
       wrapper parameter helps achieving the purpose by relying on overloading.
    */
    inline static std::uint32_t to_value(const_wrapper wrapper) noexcept
    {
        return calculateHash(wrapper.str);
    }

    /*
       Constructs a hashed string from an array of const chars.
       Forcing template resolution avoids implicit conversions. An
       human-readable identifier can be anything but a plain, old bunch of
       characters.
       Example of use:
       hashed_string hs{"my.png"};
    */
    template<std::size_t N>
    constexpr hashed_string(const char (&curr)[N]) noexcept
        : str{curr}, hash{calculateHash(curr)}
    {}

    /*
       Explicit constructor on purpose to avoid constructing a hashed
       string directly from a `const char *`.
       wrapper parameter helps achieving the purpose by relying on overloading.
    */
    explicit constexpr hashed_string(const_wrapper wrapper) noexcept
        : str{wrapper.str}, hash{calculateHash(wrapper.str)}
    {}

    //...

private:
    const char *str;
    std::uint32_t hash;
};

不幸的是,我没有看到 const_wrapper 结构的用途。它与顶部的评论有关,其中指出 "A hashed string is a compile-time tool..."?

我也不确定模板函数上方出现的注释是什么意思,是什么状态"Forcing template resolution avoids implicit conversions."有人能解释一下吗?

最后,有趣的是要注意另一个 class 如何使用此 class,该 class 维护以下类型的 std::unordered_mapstd::unordered_map<hashed_string, Resource>

另一个 class 提供了一个成员函数,可以使用键等字符串向地图添加资源。其实现的简化视图如下所示:

bool addResource(hashed_string id, Resource res)
{
    // ...
    resourceMap[id] = res;
    // ...
}

我的问题是:使用 hashed_strings 作为我们地图的键而不是 std::strings 有什么好处?使用像 hashed_strings 这样的数字类型会更有效率吗?

感谢您提供任何信息。研究这个 class 帮助我学到了很多东西。

作者试图帮助您避免重复哈希字符串时发生的意外性能问题。由于散列字符串很昂贵,您可能想做一次并将其缓存在某个地方。如果它们有一个隐式构造函数,您可以在不知道或不打算这样做的情况下重复散列相同的字符串。

所以库提供了 implicit 字符串文字构造,可以通过 constexpr 在 compile-time 处计算,但是 explicit 通常用于 const char* 的构造,因为这些通常不能在 compile-time 完成,并且您希望避免重复或意外地这样做。

考虑:

void consume( hashed_string );

int main()
{
    const char* const s = "abc";
    const auto hs1 = hashed_string{"my.png"}; // Ok - explicit, compile-time hashing
    const auto hs2 = hashed_string{s};        // Ok - explicit, runtime hashing

    consume( hs1 ); // Ok - cached value - no hashing required
    consume( hs2 ); // Ok - cached value - no hashing required

    consume( "my.png" ); // Ok - implicit, compile-time hashing
    consume( s );        // Error! Implicit, runtime hashing disallowed!
                         // Potential hidden inefficiency, so library disallows it.
}

如果我删除最后一行,您可以在 C++ Insights:

处看到编译器如何为您应用隐式转换
consume(hashed_string(hs1));
consume(hashed_string(hs2));
consume(hashed_string("my.png"));

但是由于 implict/explicit 构造函数,它拒绝对行 consume(s) 这样做。

但是请注意,这种保护用户的尝试并非万无一失。如果将字符串声明为数组而不是指针,则可能会不小心 re-hash:

const char s[100] = "abc";
consume( s );  // Compiles BUT it's doing implicit, runtime hashing. Doh.

// Decay 's' back to a pointer, and the library's guardrails return
const auto consume_decayed = []( const char* str ) { consume( str ); }
consume_decayed( s ); // Error! Implicit, runtime hashing disallowed!

这种情况不太常见,并且此类数组在传递给其他函数时通常会退化为指针,然后这些函数的行为将与上述相同。 库可以想象地对具有 if constexpr 等的字符串文字执行 compile-time 散列,并禁止对 non-literal 数组(如上面的 s)进行散列。 (这是您回馈图书馆的拉取请求!) [查看评论。]

回答您的最后一个问题:这样做的原因是为了让 hash-based 容器(如 std::unordered_map)具有更快的性能。它通过计算一次哈希并将其缓存在 hashed_string 中来最大限度地减少您必须执行的哈希数。现在,映射中的键查找只需比较键的 pre-computed 哈希值和查找字符串。