在 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_map
:std::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 哈希值和查找字符串。
我最近 运行 进入了 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_map
:std::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 哈希值和查找字符串。