1. Motivation and Scope
Heterogeneous lookup allows unordered associative containers (std :: unordered_map , std :: unordered_set , etc.) to perform lookup operations
on different (but compatible) types without creating a temporary key object. It
is an important and useful feature, particularly for performance reasons. [P0919R0] initially proposed to enable heterogeneous look up when both the
hash function Hash and the key equality comparison function Pred are tagged is_transparent . The current revision R3 revised to enable heterogeneous lookup
when the hash function has a nested tag type transparent_key_equal which is
tagged is_transparent . If heterogeneous lookup is enabled, the Pred template
parameter is entirely ignored, and Hash :: transparent_key_equal is used
instead. The use of Hash :: transparent_key_equal , however, deviates from prior
art (the is_transparent tagging mechanism for ordered associative containers),
does not address the incompatibility concerns originally expressed by LEWG
members, and adds more subtle and confusing corner cases and will likely
surprise and confuse the user.
2. Design Concerns
2.1. Consistency with Prior Art and Existing Practices
[N3657] introduced heterogeneous lookup support for ordered associative containers (std :: map , std :: set , etc) to the C++ Standard Library, which uses
the Compare :: is_transparent tag to enable. [P0919R3] deviates from this
mechanism: Hash :: transparent_key_equal must denote another type that specifies
the is_transparent tag. It also causes the Pred template parameter to be
ignored (even if it is explicitly specified), which we expect will be
unintuitive to the users.
[P0919R3] does not standardize existing practice. [SwissTables] and [F14] Hash Tables, two of the most widely used C++ hash table implementations in
Google
and Facebook, both use the conjunction of tags: heterogeneous
lookup is only enabled when both and denote a type. See the Implementation Experiences Section
for more details.
2.2. Compatibility of Hash and Pred
LEWG raised a valid concern about the compatibility of the Hash and Pred during the review of [P0919R0]. Specifically, in the R0 mechanism, should the
user neglects to tag one of the two functions as is_transparent , neglects to
specify one of them in the template parameter, or if Hash and Pred operate
on different sets of types, heterogeneous lookup would not be enabled.
[P0919R3] tries to address the compatibility concern by stipulating
compilation failures for containers with incompatible and types.
However, using does not preclude the possibility of
incompatibility--the user could still specify an incompatible key equality
comparator as . In this case, the heterogeneous lookup
methods would be unavailable (via SFINAE) or fall back to creating a temporary object, which exhibits exactly the same behavior as R0.
We argue that the compatibility concern can be addressed by the implementation via good defaults, compiler warnings, and existing compilation rules. In our experience, this has not posed a problem.
2.3. Minimizing User Confusion
With [P0919R3], the user could specify the key equality comparator in two places: as thePred template parameter or as Hash :: transparent_key_equal . R3
tries to prevent misuse by stipulating the container will fail to compile if the Pred template parameter is not the same as transparent_key_equal or (the
default) std :: equal_to < Key > . The problem is that users are likely confused or
surprised when explicitly-supplied Pred template parameter is ignored.
Pred types MyEqualTo and std :: equal_to ,
likely causing confusion for the user.
struct MyEqualTo { using is_transparent = ...; ... }; struct MyHash { using is_transparent = ...; using transparent_key_equal = MyEqualTo ; ... }; // The user explicitly uses std::equal_to<K> as the 4th template parameter, but // in fact that type is NEVER used: std :: unordered_map < K , V , MyHash , std :: equal_to < K >> m ;
In addition, allowing the and to operate on different sets of
types can be useful. It is conceivable and useful for a hashing library to have
a hasher that could operate on a wide variety of key types, e.g., for string-like types or pointer types, for most
common types. , on the other hand, may not care about the entirety of the
set of types the hasher could hash.
CommonHasher (e.g., in a common hashing
library) with a different custom Pred comparator, without either modifying the
common library (which may not be an option) or introducing yet another wrapper.
struct CommonEqualTo {...}; struct CommonHasher { using is_transparent = void ; using transparent_key_equal = CommonEqualTo ; ... }; std :: unordered_set < K , CommonHasher > s1 ; // works just fine struct MyEqualTo {...}; std :: unordered_set < K , CommonHasher , MyEqualTo > s2 ; // fails to compile
3. Impact On The Standard
This proposal modifies the unordered associative containers in< unordered_map > and < unordered_set > by overloading the lookup member functions with member
function templates.
There are no language changes.
Almost all existing C++17 code is unaffected because new member functions are
disabled from overload resolution process unless both the template
parameter and the template parameters have property.
4. Proposed Wording
The proposed changes are relative to the working draft of the standard as of [N4810].
Modify 22.2.7 [unord.req] paragraph 11.7 as follows:
(11.7)denotes a possiblya_tran value of typeconst when theX the qualified-idqualified-idsis valid and denotes a type (13.9.2),X :: hasher :: transparent_key_equal andX :: key_equal :: is_transparent are both valid and denote a type (13.9.2),X :: hasher :: is_transparent
Modify table 70 in section 22.2.7 [unord.req] as follows:
Expression Return type Assertion/note pre-/post-condition Complexity X :: key_equal if such a qualified-id is valid and denotes a type (13.9.2); otherwise,Hash :: transparent_key_equal .Pred Pred Requires: iskey_equal .CopyConstructible shall be a binary predicate that takes two arguments of typekey_equal .Key is an equivalence relation.key_equal compile time
Modify paragraph 17 in 22.2.7 [unord.req]:
If the qualified-idThe member function templatesis valid and denotes a type (12.9.2), then the program is ill-formed if either:Hash :: transparent_key_equal
qualified-id
is not valid or does not denote a type, orHash :: transparent_key_equal :: is_transparent
is a different type thanPred orequal_to < Key > .Hash :: transparent_key_equal ,find ,count , andequal_range shall not participate in overload resolution unless thecontains qualified-idqualified-idsis valid and denotes a typeHash :: transparent_key_equal andPred :: is_transparent are both valid and denote types (13.9.2).Hash :: is_transparent
Modify paragraph 3 of 22.5.4.1 [unord.map.overview] as follows:
namespace std { template < class Key , class T , class Hash = hash < Key > , class Pred = equal_to < Key > , class Allocator = allocator < pair < const Key , T >>> class unordered_map { public : // types using key_type = Key ; using mapped_type = T ; using value_type = pair < const Key , T > ; using hasher = Hash ; using key_equal = see 22.2.7 ; using key_equal = Pred ; using allocator_type = Allocator ;
Modify paragraph 3 of 22.5.5.1 [unord.multimap.overview] as follows:
namespace std { template < class Key , class T , class Hash = hash < Key > , class Pred = equal_to < Key > , class Allocator = allocator < pair < const Key , T >>> class unordered_multimap { public : // types using key_type = Key ; using mapped_type = T ; using value_type = pair < const Key , T > ; using hasher = Hash ; using key_equal = see 22.2.7 ; using key_equal = Pred ; using allocator_type = Allocator ;
Modify paragraph 3 of 22.5.6.1 [unord.set.overview] add:
namespace std { template < class Key , class Hash = hash < Key > , class Pred = equal_to < Key > , class Allocator = allocator < pair < const Key , T >>> class unordered_set { public : // types using key_type = Key ; using value_type = Key ; using hasher = Hash ; using key_equal = see 22.2.7 ; using key_equal = Pred ; using allocator_type = Allocator ;
Modify paragraph 3 of 22.5.7.1 [unord.multiset.overview] add:
namespace std { template < class Key , class Hash = hash < Key > , class Pred = equal_to < Key > , class Allocator = allocator < pair < const Key , T >>> class unordered_multiset { public : // types using key_type = Key ; using value_type = Key ; using hasher = Hash ; using key_equal = see 22.2.7 ; using key_equal = Pred ; using allocator_type = Allocator ;
5. Possible Future Extensions
This mechanism can be extended tooperator [] and other heterogeneous mutation
methods.
6. Implementation Experiences
Two widely used hash table implementations [SwissTables] and [F14] both enable heterogeneous operations when bothHash :: is_transparent and Pred :: is_transparent exists and denote a type. If either is not present or
either does not take a specific type, heterogeneous operations won’t be enabled
for that type. The user may see their code fail to compile, or will not get the
performance benefits (if the type implicitly creates a temporary key_type object). Either is a signal to double check the Hash and Pred . The vast
majority of our users who elect to use heterogeneous operations did not run into
any issue.