Heterogeneous Lookup in C++ Associative Containers
Heterogeneous lookup, introduced in C++14 for ordered containers and
expanded in C++20 to unordered containers, enables associative
containers such as std::map, std::set, and
std::unordered_map to perform search operations using types
different from the stored key type. By accepting lookup keys that can be
compared directly to the stored key without type conversion, this
feature completely eliminates the creation and destruction of expensive
temporary objects—such as dynamic memory allocations caused by
converting const char* or std::string_view to
std::string.
The Traditional Lookup Problem
In earlier C++ standards, container search member functions—including
find(), count(), lower_bound(),
and equal_range()—required an argument matching the
container's key_type:
iterator find(const Key& key);When an associative container used std::string as its
key, querying that container required an instance of
std::string. If a developer queried the container using a
string literal (const char*) or a
std::string_view:
std::set<std::string> my_set;
// ...
auto it = my_set.find("query_string");The compiler was forced to construct an implicit temporary
std::string from the string literal to satisfy the
parameter type const std::string&. Even with Small
String Optimization (SSO), constructing this temporary object incurs
overhead, including calculating the string length and copying character
buffers. If the string exceeds the SSO threshold, it triggers dynamic
heap allocation and deallocation on every lookup call.
The Mechanism of Heterogeneous Lookup
Heterogeneous lookup replaces the fixed-type parameter with a templated member function overload:
template <typename K>
iterator find(const K& x);Instead of requiring an instance of Key, the member
function accepts any type K that the container's comparator
knows how to compare against Key.
To prevent ambiguous conversions or unintended behaviors, this
feature is opt-in. A comparator signals support for heterogeneous lookup
by defining a nested type alias named is_transparent. When
the container detects Compare::is_transparent, it enables
the templated lookup overloads via SFINAE
(std::enable_if).
Ordered Containers (C++14)
For ordered associative containers (std::set,
std::multiset, std::map,
std::multimap), heterogeneous lookup requires using a
transparent comparison functor. The standard library provides
transparent versions of its standard functors when invoked with
void (the default template argument in C++14):
#include <set>
#include <string>
#include <string_view>
// Enable heterogeneous lookup using std::less<>
std::set<std::string, std::less<>> names = {"Alice", "Bob", "Charlie"};
std::string_view target = "Bob";
// Directly compares std::string_view to std::string without allocating a temporary std::string
auto it = names.find(target); Because std::less<void> provides overloaded
comparison operators that accept mixed operand types
(operator()(const std::string&, std::string_view)), the
container compares the stored element with the input reference
directly.
Unordered Containers (C++20)
In C++20, heterogeneous lookup was extended to unordered associative
containers (std::unordered_set,
std::unordered_map, etc.). Unordered containers require
both a transparent hash function and a transparent equality
predicate.
To use heterogeneous lookup in an unordered container:
- The hasher must define
is_transparentand provideoperator()overloads for both the stored key type and the search key types. - The key equal predicate must define
is_transparent(such asstd::equal_to<>).
#include <unordered_set>
#include <string>
#include <string_view>
struct StringHash {
using is_transparent = void; // Mark hasher as transparent
size_t operator()(std::string_view sv) const {
return std::hash<std::string_view>{}(sv);
}
};
// Container setup with transparent hash and transparent equality
std::unordered_set<std::string, StringHash, std::equal_to<>> dict;
void lookup(std::string_view sv) {
// Calculates hash of std::string_view and compares directly to std::string
// Zero heap allocations occur
auto it = dict.find(sv);
}Summary of Benefits
- Zero Temporary Allocations: Queries matching strings, raw pointers, or slices execute without heap allocations or buffer copies.
- Improved Cache Locality: Eliminating intermediate temporary objects reduces cache pollution and minimizes stack traffic.
- Drop-in Optimization: In ordered containers,
switching from
std::set<std::string>tostd::set<std::string, std::less<>>activates the optimization without requiring any changes to lookup call sites.