【问题标题】:In C++, why overload a function on a `const char array` and a private struct wrapping a `const char*`?在 C++ 中,为什么要重载 `const char array` 上的函数和包装 `const char*` 的私有结构?
【发布时间】:2019-06-06 21:31:34
【问题描述】:

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

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 的简单结构。下面是该类实现的简化视图来说明这一点:

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

我也不确定模板函数上方出现的 cmets 是什么意思,即“强制模板解析避免隐式转换”。有人能解释一下吗?

最后,有趣的是要注意这个类是如何被另一个类使用的,该类维护一个以下类型的std::unordered_mapstd::unordered_map&lt;hashed_string, Resource&gt;

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

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

我的问题是:使用 hashed_strings 作为我们地图的键而不是 std::strings 有什么好处?使用 hashed_strings 之类的数字类型是否更有效?

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

【问题讨论】:

  • 在汇编级别的整数比较比字符串的比较快。

标签: c++ hash overload-resolution


【解决方案1】:

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

所以该库为字符串文字提供 implicit 构造,可以在编译时通过 constexpr 计算,但通常为 const char* 提供 explicit 构造,因为那些通常不能在编译时完成,您希望避免重复或意外地这样做。

考虑:

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 等非文字数组使用它。 (有你的拉取请求要回馈图书馆!) [参见 cmets。]

回答你的最后一个问题:这样做的原因是为了让std::unordered_map 等基于散列的容器具有更快的性能。它通过计算一次哈希并将其缓存在hashed_string 中来最小化您必须执行的哈希数。现在,映射中的键查找只需比较键的预先计算的哈希值和查找字符串。

【讨论】:

  • 我刚刚意识到const_wrapper 是通常所说的“代理类”。 Scott Meyers 在他的书“更有效的 C++”的第 5 项中谈到了这种类型的类。真的很有趣!
  • 我自己不会称它为proxy,除非在该术语的最广泛定义中,因为它并不是真正打算表示其他对象的接口。相反,它与重载系统玩了一个小把戏,以防止某些重载解决。我会坚持使用“包装器”,但 YMMV。
  • PS,我添加了一个关于char 数组的库保护漏洞的“淋浴想法”。
  • 唉,你可能是对的。我梦想着使用std::is_constant_evaluated()this episode of C++ WeeklyHere 是我试图勾勒出我的想法,但我承认我还没有完全完成它。它可能根本不可行,或者可能需要运行时异常或其他东西,而不是首选的编译时错误。
  • 我也尝试过测试 C++20 的 consteval,但我找不到支持它的编译器(包括 Godbolt 和 VS2019 上的所有实验)!所以,也许有一天它会是可行的,但今天不是那一天。 (您的 PR 可以是一个宏 MY_CONSTEVAL,在支持的情况下变为 consteval,在不支持的情况下变为 constexpr。它今天会通过所有测试,并且只有在编译器赶上时才会失败。;-) 不太推荐。)
猜你喜欢
  • 1970-01-01
  • 2010-10-27
  • 2018-03-27
  • 2014-02-15
  • 1970-01-01
  • 1970-01-01
  • 2012-12-26
  • 2010-09-09
相关资源
最近更新 更多