std::hash - built-in타입이 아닌 경우 직접 정의해보기
C++/C++ 2022. 8. 9. 01:35 |std::hash
간단히 말하면,
어떤 함수 객체에 단일 인자를 전달했을 때 특정 반환 값이 출력되도록하는 기능이다.
Built-in 타입에 대한 std::hash 특수화 지원
해쉬 테이블 기능을 제공하는 std::unordered_map을 사용할 때
Key로 std::hash<> 특수화가 존재하지 않는 타입을 건내주면 아래와 같은 컴파일 에러가 발생한다.
Key로 전달된 타입에 대하여
std::hash<> 템플릿 특수화를 수행하지 못하여 발생한 오류이다.
참고로 C++은 몇가지 타입에 대해서는 미리 정의된 std::hash<> 템플릿이 존재한다.
std::hash가 특수화로 지원하는 타입은 다음과 같다.
std::hash<> 특수화 추가 정의하기
std::set<pair<int,int>>을 Key로 사용하는 std::unordered_map을 생성하려고 한다.
std::unordered_map<std::set<pair<int,int>>, int > temp;
std::hash< std::set<pair<int,int>>> 를 정의해주지 않았기 때문에 컴파일 에러가 발생할 것이다.
따라서 std::hash< std::set<pair<int,int>>> 를 정의해보자.
아래 코드는 VS에서 제공하는 Hash 기법을 사용하여 정의하였다.
namespace std
{
template<>
struct hash<set<pair<int, int>>>
{
size_t operator()(const set<pair<int, int>>& other) const noexcept
{
return ArrayHasher(14695981039346656037ULL, other);
}
inline size_t ArrayHasher(size_t _Val, const set<pair<int, int>>& other) const noexcept
{
for (const auto& it : other)
{
_Val ^= static_cast<size_t>(it.first ^ it.second);
_Val *= 1099511628211ULL;
}
return _Val;
}
};
}
int main()
{
unordered_map<set<pair<int, int>>, int> myUmap;
set<pair<int, int>> mySet;
mySet.insert(make_pair(3, 5));
myUmap[mySet]++;
}
위와 같이 namespace std에 직접 std::hash<> 특수화를 정의하면,
std::unordered_map에 Hash를 직접 전달하지 않아도 된다.
또는 아래와 같이 std::unordered_map의 Hash인자로 함수 객체를 명시적으로 전달해도 된다.
struct CustomedHasher
{
size_t operator()(const set<pair<int, int>>& other) const noexcept
{
// 숫자 14695981039346656037 : Hash 충돌 최소화 하면서 성능을 극대화하기 위해 사용 됨
return ArrayHasher(14695981039346656037ULL, other);
}
inline size_t ArrayHasher(size_t _Val, const set<pair<int, int>>& other) const noexcept
{
for (const auto& it : other)
{
_Val ^= static_cast<size_t>(it.first ^ it.second);
// 숫자 1099511628211 : Hash 충돌 최소화 하면서 성능을 극대화하기 위해 사용 됨
_Val *= 1099511628211ULL;
}
return _Val;
}
};
int main()
{
unordered_map<set<pair<int, int>>, int, CustomedHasher> myUmap;
set<pair<int, int>> mySet;
mySet.insert(make_pair(3, 5));
myUmap[mySet]++;
}
해시 함수 입력 값이 배열이 아닌 경우 다음과 같이 간단하게 만들 수 있음
struct CustomedHasher
{
size_t operator()(const pair<int, int>& other) const noexcept
{
size_t _Val = 14695981039346656037ULL; //offset-basis
_Val ^= static_cast<size_t>(other.first);
_Val ^= static_cast<size_t>(other.second);
_Val *= 1099511628211ULL; // prime
return _Val;
}
};
해시 충돌 최소화
struct PoorHasher {
std::size_t operator()(const std::set<pair<int, int>>& set) const noexcept {
return std::size_t x1{ std::hash<size_t>{}(set.size()) };
}
};
누군가의 코드를 봤는데 Hash를 위와 같이 정의해서 전달하더라...
set.size()가 같으면 해시 충돌이 발생할 수 밖에 없다.
std::hash 구현 요구 사항에도 보면 충돌 확률을 1 / 2^64만큼으로 줄이라고 되어 있다.
CustomedHaser에 보면 알 수 없이 긴 숫자가 충돌 최소화 & 성능 극대화 역할을 돕는다.
PoorHasher의 세번째 줄에 std::hash<size_t>{} 뒤에 (set.size()) 가 붙어있는데 이것이 바로
함수 객체 역할을 하는 std::hash가 가지고 있어야할 operator() 함수이다.
잘 모르고 보면 std::hash<size_t>에 대한 직접 초기화 문법 뒤에
이상하게 (set.size())가 붙어있어서 당황할 수 있다고 생각해서
적어보았다.
std::size_t x1{ std::hash<size_t>{}(set.size()) }; 코드는 아래 코드를 줄인 것이다.
auto hash = std::hash<size_t>{};
std::size_t x1 { hash(set.size()) }; // hash의 operator()를 호출한 것
VS에서 제공하는 std::hash 특수화 관련
// These FNV-1a utility functions are extremely performance sensitive,
// check examples like that in VSO-653642 before making changes.
#if defined(_WIN64)
_INLINE_VAR constexpr size_t _FNV_offset_basis = 14695981039346656037ULL;
_INLINE_VAR constexpr size_t _FNV_prime = 1099511628211ULL;
#else // defined(_WIN64)
_INLINE_VAR constexpr size_t _FNV_offset_basis = 2166136261U;
_INLINE_VAR constexpr size_t _FNV_prime = 16777619U;
#endif // defined(_WIN64)
// 배열 형태의 자료구조를 해싱하기
_NODISCARD inline size_t _Fnv1a_append_bytes(size_t _Val, const unsigned char* const _First,
const size_t _Count) noexcept { // accumulate range [_First, _First + _Count) into partial FNV-1a hash _Val
for (size_t _Idx = 0; _Idx < _Count; ++_Idx) {
_Val ^= static_cast<size_t>(_First[_Idx]);
_Val *= _FNV_prime;
}
return _Val;
}
template <class _Ty>
_NODISCARD size_t _Fnv1a_append_range(const size_t _Val, const _Ty* const _First,
const _Ty* const _Last) noexcept { // accumulate range [_First, _Last) into partial FNV-1a hash _Val
static_assert(is_trivial_v<_Ty>, "Only trivial types can be directly hashed.");
const auto _Firstb = reinterpret_cast<const unsigned char*>(_First);
const auto _Lastb = reinterpret_cast<const unsigned char*>(_Last);
return _Fnv1a_append_bytes(_Val, _Firstb, static_cast<size_t>(_Lastb - _Firstb));
}
template <class _Kty>
_NODISCARD size_t _Fnv1a_append_value(
const size_t _Val, const _Kty& _Keyval) noexcept { // accumulate _Keyval into partial FNV-1a hash _Val
static_assert(is_trivial_v<_Kty>, "Only trivial types can be directly hashed.");
return _Fnv1a_append_bytes(_Val, &reinterpret_cast<const unsigned char&>(_Keyval), sizeof(_Kty));
}
template <class _Kty>
_NODISCARD size_t _Hash_representation(const _Kty& _Keyval) noexcept { // bitwise hashes the representation of a key
return _Fnv1a_append_value(_FNV_offset_basis, _Keyval);
}
template <class _Kty>
_NODISCARD size_t _Hash_array_representation(
const _Kty* const _First, const size_t _Count) noexcept { // bitwise hashes the representation of an array
static_assert(is_trivial_v<_Kty>, "Only trivial types can be directly hashed.");
return _Fnv1a_append_bytes(
_FNV_offset_basis, reinterpret_cast<const unsigned char*>(_First), _Count * sizeof(_Kty));
}
template <class _Kty>
struct hash;
template <class _Kty, bool _Enabled>
struct _Conditionally_enabled_hash { // conditionally enabled hash base
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef _Kty _ARGUMENT_TYPE_NAME;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef size_t _RESULT_TYPE_NAME;
_NODISCARD size_t operator()(const _Kty& _Keyval) const
noexcept(noexcept(hash<_Kty>::_Do_hash(_Keyval))) /* strengthened */ {
return hash<_Kty>::_Do_hash(_Keyval);
}
};
template <class _Kty>
struct _Conditionally_enabled_hash<_Kty, false> { // conditionally disabled hash base
_Conditionally_enabled_hash() = delete;
_Conditionally_enabled_hash(const _Conditionally_enabled_hash&) = delete;
_Conditionally_enabled_hash(_Conditionally_enabled_hash&&) = delete;
_Conditionally_enabled_hash& operator=(const _Conditionally_enabled_hash&) = delete;
_Conditionally_enabled_hash& operator=(_Conditionally_enabled_hash&&) = delete;
};
template <class _Kty>
struct hash
: _Conditionally_enabled_hash<_Kty,
!is_const_v<_Kty> && !is_volatile_v<_Kty> && (is_enum_v<_Kty> || is_integral_v<_Kty> || is_pointer_v<_Kty>)> {
// hash functor primary template (handles enums, integrals, and pointers)
static size_t _Do_hash(const _Kty& _Keyval) noexcept {
return _Hash_representation(_Keyval);
}
};
template <>
struct hash<float> {
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef float _ARGUMENT_TYPE_NAME;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef size_t _RESULT_TYPE_NAME;
_NODISCARD size_t operator()(const float _Keyval) const noexcept {
return _Hash_representation(_Keyval == 0.0F ? 0.0F : _Keyval); // map -0 to 0
}
};
template <>
struct hash<double> {
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef double _ARGUMENT_TYPE_NAME;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef size_t _RESULT_TYPE_NAME;
_NODISCARD size_t operator()(const double _Keyval) const noexcept {
return _Hash_representation(_Keyval == 0.0 ? 0.0 : _Keyval); // map -0 to 0
}
};
template <>
struct hash<long double> {
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef long double _ARGUMENT_TYPE_NAME;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef size_t _RESULT_TYPE_NAME;
_NODISCARD size_t operator()(const long double _Keyval) const noexcept {
return _Hash_representation(_Keyval == 0.0L ? 0.0L : _Keyval); // map -0 to 0
}
};
template <>
struct hash<nullptr_t> {
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef nullptr_t _ARGUMENT_TYPE_NAME;
_CXX17_DEPRECATE_ADAPTOR_TYPEDEFS typedef size_t _RESULT_TYPE_NAME;
_NODISCARD size_t operator()(nullptr_t) const noexcept {
void* _Null{};
return _Hash_representation(_Null);
}
};
'C++ > C++' 카테고리의 다른 글
[메모리] 메모리 프로파일링 (0) | 2022.04.06 |
---|---|
Variadic Template 가변인자 템플릿의 Recursive call 문법 (0) | 2022.03.11 |
멤버 함수 포인터(Member Function Pointer) (0) | 2022.02.11 |
(작성 중) Scoped static 과 Singleton (0) | 2022.02.02 |
MSVC++ Tips (0) | 2022.01.22 |