如何自定义 `std::unordered_set、std::unordered_map` 中元素的哈希函数和相等函数
方法 1:定义独立的哈希函数对象
-
自定义类型
假设需要存储包含name
和age
的结构体Person
:struct Person { std::string name; int age; };
-
定义独立的哈希函数对象(仿函数)
需要定义一个结构体 PersonHash,它覆盖了自定义的哈希函数即 重载operator()
,返回size_t
类型:struct PersonHash { size_t operator()(const Person& p) const { // 组合 name 的哈希值和 age 的哈希值 return std::hash<std::string>()(p.name) ^ (std::hash<int>()(p.age) << 1); } };
注意:实际中应使用更可靠的哈希组合方式(如
boost::hash_combine
)。 -
自定义相等比较函数(仿函数)
定义一个结构体PersonEqual,它覆盖了自定义的相等比较函数即 重载operator()
,返回bool
类型:struct PersonEqual { bool operator()(const Person& a, const Person& b) const { return a.name == b.name && a.age == b.age; } }; //也可以放在自定义类型中,这样就不用写在unordered_set的初始化语句中了
-
实例化 unordered_set
模板参数中指定PersonHash
和PersonEqual
:std::unordered_set<Person, PersonHash, PersonEqual> person_set;
完整示例
#include <iostream>
#include <unordered_set>
#include <string>
struct Person {
std::string name;
int age;
};
// 自定义哈希函数
struct PersonHash {
size_t operator()(const Person& p) const {
return std::hash<std::string>{}(p.name) ^ (std::hash<int>{}(p.age) << 1);
}
};
// 自定义相等比较函数
struct PersonEqual {
bool operator()(const Person& a, const Person& b) const {
return a.name == b.name && a.age == b.age;
}
};
int main() {
std::unordered_set<Person, PersonHash, PersonEqual> person_set;//显式指定哈希函数
person_set.insert({"Alice", 30});
person_set.insert({"Bob", 25});
// 检查是否存在 Alice, 30
if (person_set.find({"Alice", 30}) != person_set.end()) {
std::cout << "Found Alice!" << std::endl;
}
return 0;
}
/*
如果把相等比较函数放入结构体person中,则会变成这样:
struct Person {
std::string name;
int age;
bool operator()(const Person& a, const Person& b) const {
return a.name == b.name && a.age == b.age;
}
};
//此时在定义时就可以少写一个结构体了
std::unordered_set<Person, PersonHash> person_set;
*/
方法 2:特化 std::hash
模板
步骤
- 定义自定义类型(如
Person
类) - 在
namespace std
中特化std::hash
- 实现
operator()
函数,组合各成员变量的哈希值 - 提供
operator==
重载(哈希容器需要)
示例代码
#include <string>
#include <functional>
// 自定义类型
struct Person {
std::string name;
int age;
// 必须提供相等运算符
bool operator==(const Person& other) const {
return name == other.name && age == other.age;
}
};
// 特化 std::hash
namespace std {
template<>
struct hash<Person> {
size_t operator()(const Person& p) const {
// 组合 name 和 age 的哈希值
size_t name_hash = hash<string>{}(p.name);
size_t age_hash = hash<int>{}(p.age);
// 使用异或组合哈希(可改进为更复杂的组合方式)
return name_hash ^ (age_hash << 1);
}
};
}
// 使用示例
#include <unordered_set>
int main() {
std::unordered_set<Person> person_set; // 自动使用特化的 std::hash<Person>
return 0;
}
关键注意事项
- 哈希组合技巧:简单异或(
^
)容易导致碰撞,建议使用类似hash_combine
的算法:template <class T> inline void hash_combine(std::size_t& seed, const T& v) { seed ^= std::hash<T>{}(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2); }
- 哈希质量:确保不同对象的哈希值尽可能唯一,否则会影响容器性能
- 命名空间限制:特化【注】
std::hash
必须在namespace std
中。由于
std::hash原生定义在namespace std中,任何针对它的显式特化(如std::hash)也必须置于该命名空间,否则编译器将视为未定义
其他实现方式
- 使用 Lambda 表达式(需 C++20)
通过构造函数传递哈希和比较函数:
auto hash = [](const Person& p) { /*...*/ };
auto equal = [](const Person& a, const Person& b) { /*...*/ };
std::unordered_set<Person, decltype(hash), decltype(equal)> set(10, hash, equal);
注:特化(Specialization) 和实例化是不一样的,实例化是把模板赋予具体的类型了,而特化仍然是一个模板,只不过是一个特殊的模板。也就是说,特化之后也会被实例化(而且比通用的模板更优先被匹配)。所以,特化的意义就在于:它可以为特定的实例类型 提供定制化的实现(不同于其他的同样模板)
-
模板特化
在C++等支持泛型编程的语言中,特化指为泛型模板提供特定类型或参数的定制化实现。例如:// 通用模板 template <typename T> class MyContainer { /*...*/ }; // 全特化:针对int类型的完全定制实现 template <> class MyContainer<int> { /*...*/ };
这里
template <>
标识了全特化,即完全覆盖通用模板的行为。 -
部分特化(偏特化)
与全特化不同,部分特化允许仅对模板参数的部分特性进行定制。例如:// 通用模板 template <typename T, typename U> class Pair { /*...*/ }; // 偏特化(2类) //1.指定部分参数 template <typename T> class Pair<T, T> { /*...*/ }; //2.通过模式匹配约束参数类型 template <class T> class vector<T*> { ... }; // 对指针类型的偏特化 //这里的T*表示所有指针类型,未完全固定参数,但通过模式限定了参数的形式。
特化的意义
- 性能优化:为特定数据类型(如
int
)提供高度优化的实现,避免泛型代码的运行时开销。 - 功能扩展:针对特殊需求调整通用逻辑,例如为指针类型添加内存管理逻辑。
- 约束行为:限制某些类型的使用方式,如禁止拷贝语义的类型。
与其他概念的对比
- 多态:特化属于编译时多态,而虚函数实现的是运行时多态。
- 重载:特化修改了模板本身的行为,而函数重载通过参数列表区分不同实现。