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

标签 c++ hash overload-resolution

我最近在 ENTT 图书馆遇到了一个有趣的类(class)。此类用于计算字符串的哈希值,如下所示:

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

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

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

/*
   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 的目的结构。它与顶部的注释有关吗,该注释指出“哈希字符串是编译时工具......”?

我也不确定模板函数上方出现的注释的含义,其中指出“强制模板解析避免隐式转换”。有谁能解释一下吗?

最后,值得注意的是另一个维护 std::unordered_map 的类如何使用该类。以下类型:std::unordered_map<hashed_string, Resource>

这个其他类提供了一个成员函数,可以使用键等字符串将资源添加到 map 中。其实现的简化 View 如下所示:

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

我的问题是:使用 hashed_strings 作为映射的键而不是 std::strings 有什么优势?使用像 hashed_strings 这样的数字类型是否更有效?

感谢您提供任何信息。学习这门课让我学到了很多东西。

最佳答案

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

因此,该库为字符串文字提供了隐式构造,可以通过constexpr在编译时计算。但 const char*显式构造一般来说,因为这些通常不能在编译时完成,并且您希望避免重复或意外地这样做。

考虑:

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"));

但它拒绝对 consume(s) 行这样做因为隐式/显式构造函数。

但请注意,这种保护用户的尝试并非万无一失。如果将字符串声明为数组而不是指针,则可能会意外地重新散列:

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 的字符串文字强制执行编译时哈希。等等,并禁止它用于非文字数组,如 s多于。 (这是您要回馈给图书馆的拉取请求!) [查看评论。]

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

关于c++ - 在 C++ 中,为什么要重载 `const char array` 上的函数和包装 `const char*` 的私有(private)结构?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56446068/

相关文章:

c++ - 使用 MySQL Connector/C++ 调试项目时出现运行时错误

c++ - 是否有任何将资源嵌入 Linux 可执行镜像的标准方法?

c++ - 是否可以在 Visual Studio 2013 中设置多个条件断点?

language-agnostic - 哈希码和校验和 - 有什么区别?

c++ - 为范围内的所有数字返回相同值的哈希?

c++ - 使用基类的所有重载

c - 我将如何比较 2 个 unsigned char 数组?

c++ - 解析带默认参数的虚函数

c++ - 查看命名空间的重载解决方案

c# - 重载方法中的 StackOverflowException