异构比较查找
1. 核心概念
异构比较查找指:在关联式容器里,用“与键类型不同但可比较/可哈希的类型”进行查找、计数、边界查询与删除,而无需构造临时的键对象或产生分配开销。
典型收益:用 std::string_view/const char* 在 std::map<std::string, …> 或 std::unordered_map<std::string, …> 中查找,避免创建临时 std::string。
2. 适用范围与标准版本
- 有序容器(树):
std::map/std::set/std::multimap/std::multiset
自 C++14 起支持异构查找,前提是比较器是透明的。 - 无序容器(哈希):
std::unordered_map/std::unordered_set及其 multi 变体
自 C++20 起支持异构查找,前提是哈希器与相等谓词均为透明,且它们能接受异构类型。
3. 如何“启用”异构查找
3.1 有序容器:使用透明比较器
-
直接使用标准的
std::less<>(注意带尖括号空模板参数),它是透明的。 -
或自定义比较器,需:
- 提供模板/重载,能比较键类型与目标查询类型;
- 定义嵌套别名
using is_transparent = void;表示“透明”。
透明后,以下成员可接受异构类型:find / count / lower_bound / upper_bound / equal_range / erase(key)(C++14 起)/ contains(C++20 起)。
3.2 无序容器:透明哈希与透明相等
-
提供可接收异构类型的哈希器与相等谓词;并各自声明
using is_transparent = void; -
常见做法(键为
std::string):- 哈希器用
std::hash<std::string_view>(其operator()接收std::string_view,可从std::string/const char*隐式转换获得); - 相等谓词用
std::equal_to<>(透明)。
- 哈希器用
-
也可自定义哈希器/相等谓词以支持大小写不敏感等策略,但要确保二者一致性(“相等 ⇒ 同哈希”)。
4. 实战示例
示例 A:在 std::map<std::string, int> 中用 std::string_view/const char* 查找(C++14+)
#include <map>
#include <string>
#include <string_view>
#include <iostream>int main() {// 使用透明比较器 std::less<>,而不是 std::less<std::string>std::map<std::string, int, std::less<>> m{{"apple", 1}, {"banana", 2}, {"cherry", 3}};// 用 string_view 查找:不会分配内存或构造临时 std::stringstd::string_view key_sv = "banana";if (auto it = m.find(key_sv); it != m.end())std::cout << it->first << " => " << it->second << '\n';// 用 const char* 查找if (auto it = m.find("cherry"); it != m.end())std::cout << it->first << " => " << it->second << '\n';// 异构边界查询auto lb = m.lower_bound("banana"); // const char* 也可if (lb != m.end())std::cout << "lower_bound: " << lb->first << '\n';
}
要点:
- 关键在于
std::less<>是透明的并接受异构比较; - 若用默认的
std::less<std::string>,则不具备异构查找能力。
示例 B:大小写不敏感的 std::set<std::string> 异构查找(自定义透明比较器,C++14+)
#include <set>
#include <string>
#include <string_view>
#include <cctype>struct ci_less {using is_transparent = void; // 声明透明// 统一转小写逐字符比较(示例简化,未处理 locale)static int cmp(std::string_view a, std::string_view b) {auto tolow = [](unsigned char c){ return std::tolower(c); };size_t n = std::min(a.size(), b.size());for (size_t i = 0; i < n; ++i) {int da = tolow(a[i]), db = tolow(b[i]);if (da != db) return da - db;}return (a.size() < b.size()) ? -1 : (a.size() > b.size());}bool operator()(std::string_view a, std::string_view b) const {return cmp(a, b) < 0;}// 若需要,也可额外提供重载以支持更多类型
};int main() {std::set<std::string, ci_less> S = {"Apple", "banana", "Cherry"};// 用 const char* 查auto it1 = S.find("BANANA");// 用 string_view 查std::string_view key = "cherry";auto it2 = S.find(key);
}
要点:
- 比较器需对所有可能参与比较的类型给出严格弱序(strict weak ordering)。
is_transparent是开启异构接口的“开关”。
示例 C:std::unordered_map<std::string, int> 的异构查找(C++20+)
#include <unordered_map>
#include <string>
#include <string_view>
#include <iostream>
#include <functional>struct sv_hash {using is_transparent = void; // 透明using is_avalanching = void; //(可选,给库优化用)size_t operator()(std::string_view sv) const noexcept {return std::hash<std::string_view>{}(sv);}// 允许以 std::string、const char* 直接入参(均可隐式转为 string_view)size_t operator()(const char* s) const noexcept {return std::hash<std::string_view>{}(std::string_view{s});}size_t operator()(const std::string& s) const noexcept {return std::hash<std::string_view>{}(s);}
};struct sv_equal {using is_transparent = void; // 透明bool operator()(std::string_view a, std::string_view b) const noexcept {return a == b;}bool operator()(const char* a, std::string_view b) const noexcept {return std::string_view{a} == b;}bool operator()(std::string_view a, const char* b) const noexcept {return a == std::string_view{b};}
};int main() {// 键类型仍是 std::string,但哈希与相等谓词对异构类型透明std::unordered_map<std::string, int, sv_hash, sv_equal> um{{"alpha", 1}, {"beta", 2}, {"gamma", 3}};if (auto it = um.find("beta"); it != um.end())std::cout << it->first << " => " << it->second << '\n';std::string_view key = "gamma";if (um.contains(key))std::cout << "contains gamma\n";// C++20 起 erase 也可用异构键um.erase("alpha");
}
要点:
- 无序容器需要“哈希器 + 相等谓词”同时透明,并能接收异构类型;
- 切记两者要一致:相等则哈希相等(或至少冲突概率相当),否则行为/性能不可预期。
5. 常见问题与陷阱
-
别忘了透明性:
- 有序容器用
std::less<>或自定义并含is_transparent的比较器; - 无序容器的哈希器与相等谓词都要提供
is_transparent,且能处理异构入参。
- 有序容器用
-
std::string_view的生命周期:
查找阶段用string_view没问题,但往容器插入时仍要保证最终存储对象(如std::string)拥有自己的内存;不要把指向临时的string_view保存下来。 -
顺序一致性/等价关系(有序容器):
自定义比较器必须对所有可能比较的类型定义同一套严格弱序。 -
哈希/相等一致性(无序容器):
“相等 ⇒ 哈希相等(或同分布)”必须在所有参与类型上成立。 -
标准版本差异:
- 树型容器异构查找:C++14 起成熟可用;
- 哈希容器异构查找:C++20 起才在标准层面完善。旧编译器/库可能不完全支持。
6. 何时应使用
- 频繁用只读视图(如
std::string_view)在std::string容器中查找; - 需要避免临时对象构造与内存分配;
- 需要
lower_bound/upper_bound等在不制造临时键的情况下执行边界查询。
7. 小结
- 有序容器:把比较器换成
std::less<>(或自定义透明比较器)即可获得异构查找。 - 无序容器:提供透明且可接收异构类型的哈希器与相等谓词(常配合
std::string_view)。 - 这样可以减少拷贝与分配,显著优化查找路径上的延迟与吞吐。
