1. Motivation
[N3657] and [P0919R0] introduced heterogeneous lookup support for ordered and unordered
associative containers in C++ Standard Library. As a result, a temporary key object is not
created when a different (but comparable) type is provided as a key to the member function.
But there are no heterogeneous
and
overloads.
Consider the following example:
void foo ( std :: map < std :: string , int , std :: less < void >>& m ) { const char * lookup_str = "Lookup_str" ; auto it = m . find ( lookup_str ); // matches to template overload // some other actions with iterator m . erase ( lookup_str ); // causes implicit conversion }
Function
accepts a reference to the
object. A
comparator for the map is
, which provides
valid
qualified-id, so the
allows using heterogeneous overloads with
template
parameter for lookup functions, such as
,
,
, etc.
In the example above, the
call does not create a temporary object of
the
to perform a lookup. But, the
call causes implicit
conversion from
to
. The allocation and construction (as well as
subsequent destruction and deallocation) of the temporary object of
or any custom object can be quite expensive and reduce the program performance.
Erasure from the STL associative containers with the
instance of the type that is different
from
is possible with the following code snippet:
auto eq_range = container . equal_range ( key ); auto previous_size = container . size (); container . erase ( eq_range . first , eq_range . second ); auto erased_count = container . size () - previous_size ;
where
is false
.
determines the count of erased items and
is either:
-
An ordered associative container in which
is a valid qualified-id.key_compare :: is_transparent -
An unordered associative container in which
andhasher :: is_transparent
are valid qualified-ids.key_equal :: is_transparent
The code above is a valid alternative for the heterogeneous
overload. But
heterogeneous
would allow to do the same things more efficiently, without traversing
the interval [
) twice (the first time to determine the equal range
and the second time for erasure). It adds a performance penalty for the containers with non-unique keys
(like
,
, etc.) where the number of elements with the same key can be quite large.
Note: This paragraph and § 4 Performance evaluation contain examples for the
method.
But the problems and benefits are similar for both
and
member functions.
2. Prior Work
Possibility to add heterogeneous
overload was reviewed in the [N3465]. But it was
found, that heterogeneous
brakes backward compatibility and causes wrong overload
resolution for the case when an
is passed as the argument. The
type is
implicitly converted into
and the following overload of the erase method is called:
iterator erase ( const_iterator pos )
If there was the following heterogeneous overload of the erase method:
template < class K > size_type erase ( const K & x );
template overload would be chosen in C++14 when an
object passed as the
argument. So, it can cause the wrong effect or compilation error for legacy code.
C++17 introduces a new overload for
method, which accepts exactly an object of
type
as an argument:
iterator erase ( iterator pos )
This change intended to fix the ambiguity issue [LWG2059] in the
method overloads (for
and for
) when a
object can be constructed from the
.
Adding the overload with the
argument solves the problem described above,
but overload with template
parameter still will be chosen when passing an object of a type,
which is implicitly convertible to either
or
that is not what
user might expect.
3. Proposal overview
We analyzed the prior work and basing on that we propose to add heterogeneous overloads for
and
methods in
,
,
,
,
,
,
and
:
template < class K > size_type erase ( const K & x );
and
template < class K > node_type extract ( const K & x );
To maintain backward compatibility and avoid wrong overload resolution or compilation errors
these overloads should impose extra restrictions on the type
.
For ordered associative containers these overloads should participate in overload resolution only if all the following statements are true:
-
Qualified-id
is valid and denotes a type.Compare :: is_transparent -
The type
is not convertible to theK
.iterator -
The type
is not convertible to theK
.const_iterator
where
is a type of comparator passed to an ordered container.
For unordered associative containers these overloads should participate in overload resolution if all the following statements are true:
-
Qualified-id
is valid and denotes a type.Hash :: is_transparent -
Qualified-id
is valid and denotes a type.Pred :: is_transparent -
The type
is not convertible to theK
.iterator -
The type
is not convertible to theK
.const_iterator
where
and
are types of hash function and key equality predicate passed to
an unordered container respectively.
4. Performance evaluation
We estimated the performance impact on two synthetic benchmarks:
-
Erase all elements consistently from the
, filled by 10000 values with unique keys. Size of eachstd :: unordered_map < std :: string , int >
key is 1000.std :: string -
Erase all elements from the
filled by 10000 values with duplicated keys. Size of eachstd :: unordered_multimap < std :: string , int >
key is 1000.std :: string
To do that we have implemented heterogeneous
method for
and
on the base of LLVM Standard Library implementation.
We have compared the performance of three possible erasure algorithms:
-
Erasure by
object.key_type -
Erasure by the pair of iterators, obtained by the heterogeneous
.equal_range -
Heterogeneous erasure with the
as an argument.std :: string_view
The benchmark for
shows that the erasure by the pair of iterators (as well
as heterogeneous erasure) increases performance by ~20%.
The benchmark for
shows the same performance increase for
erasure by the pair of iterators and an additional performance increase by ~10% for
heterogeneous erasure (due to double traversal of
).
To do the additional analysis with different memory allocation source we took an open-source
application pmemkv. It is an embedded key/value data
storage designed for emergent persistent memory. pmemkv has several storage engines optimized
for different use cases. For the analysis we chose vsmap engine that is built on
data structure with allocator for persistent memory from the memkind library.
with the memkind allocator was used as a key and value type.
using storage_type = std :: basic_string < char , std :: char_traits < char > , libmemkind :: pmem :: allocator < char >> ; using key_type = storage_type ; using mapped_type = storage_type ; using map_allocator_type = libmemkind :: pmem :: allocator < std :: pair < const key_type , mapped_type >> ; using map_type = std :: map < key_type , mapped_type , std :: less < void > , std :: scoped_allocator_adaptor < map_allocator_type >> ;
Initial implementation of
method of vsmap engine was the following:
status vsmap :: remove ( std :: string_view key ) { std :: size_t erased = c . erase ( key_type ( key . data (), key . size (), a )); return ( erased == 1 ) ? status :: OK : status :: NOT_FOUND ; }
The initial implementation explicitly creates temporary object of
when
method is called. To estimate performance impact of the heterogeneous
overload
we re-designed
operation of the vsmap engine in the following way:
status vsmap :: remove ( std :: string_view key ) { auto it = c . find ( key ); if ( it != c . end ()) { c . erase ( it ); return status :: OK ; } return status :: NOT_FOUND ; }
To understand the performance impact we used pmemkv utility.
We run deleterandom benchmark on prefilled storage
and measured throughput as a number of operations per second. We executed the test with
different key sizes (16 bytes, 200 bytes, 1024 bytes). The chart below shows performance increase,
comparing to initial implementation, for all tests. The throughput of the
operation increased
up to 9x for the 1024 bytes key.
5. Formal wording
Modify Table 69 "Associative container requirements" in [tab:container.assoc.req] as follows:
Table 69 — Associative container requirements (in addition to container)
Expression Return type Assertion/ note/ pre-/ post-condition Complexity [...] a.extract(k) node_type Effects: removes the first element in the container with key equivalent to k.
Returns: A node_type owning the element if found, otherwise an empty node_typelog(a.size()) a_tran.extract(ke) node_type Effects: removes the first element in the container with key r such that
!c(r, ke) && !c(ke, r).
Returns: A node_type owning the element if found, otherwise an empty node_typelog(a_tran.size()) [...] a.erase(k) size_type Effects: Erases all elements in the container with key equivalent to k.
Returns: The number of erased elementslog(a.size()) + a.count(k)) a_tran.erase(ke) size_type Effects: Erases all elements in the container with key r such that
!c(r, ke) && !c(ke, r).
Returns: The number of erased elementslog(a_tran.size()) + a_tran.count(ke)) [...]
Add paragraph 16 in section 22.2.6 [associative.reqmts]:
[...]The member function templates erase and extract shall participate in overload resolution only if all of the following conditions are true:[...]
(16.1) - the qualified-id Compare::is_transparent is valid and denotes a type
(16.2) - a type of template parameter K is not convertible to iterator (std::is_convertible_v<K, iterator> != true)
(16.3) - a type of template parameter K is not convertible to const_iterator (std::is_convertible_v<K, const_iterator> != true)
Modify 22.4.4.1 [map.overview], class template map synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.4.5.1 [multimap.overview], class template multimap synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.4.6.1 [set.overview], class template set synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.4.7.1 [multiset.overview], class template multiset synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify Table 70 "Unordered associative container requirements" in [tab:container.hash.req] as follows:
Table 70 — Unordered associative container requirements (in addition to container)
Expression Return type Assertion/ note/ pre-/ post-condition Complexity [...] a.extract(k) node_type Effects: removes an element in the container with key equivalent to k.
Returns: A node_type owning the element if found, otherwise an empty node_typeAverage case O(1), worst case O(a.size()). a_tran.extract(ke) node_type Effects: removes an element in the container with key equivalent to ke.
Returns: A node_type owning the element if found, otherwise an empty node_typeAverage case O(1), worst case O(a_tran.size()). [...] a.erase(k) size_type Effects: Erases all elements with key equivalent to k.
Returns: The number of elements erasedAverage case O(a.count(k)).
Worst case O(a.size()).a_tran.erase(ke) size_type Effects: Erases all elements with key equivalent to ke.
Returns: The number of elements erasedAverage case O(a_tran.count(k)).
Worst case O(a_tran.size()).[...]
Add paragraph 19 in section 22.2.7.1 [unord.req]:
[...]The member function templates erase and extract shall participate in overload resolution only if all of the following conditions are true:[...]
(19.1) - the qualified-ids Pred::is_transparent and Hash::is_transparent are both valid and denote types
(19.2) - a type of template parameter K is not convertible to iterator (std::is_convertible_v<K, iterator> != true)
(19.3) - a type of template parameter K is not convertible to const_iterator (std::is_convertible_v<K, const_iterator> != true)
Modify 22.5.4.1 [unord.map.overview], class template unordered_map synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.5.5.1 [unord.multimap.overview], class template unordered_multimap synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.5.6.1 [unord.set.overview], class template unordered_set synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
Modify 22.5.7.1 [unord.multiset.overview], class template unordered_multiset synopsis, as indicated
[...]node_type extract ( const_iterator position ); node_type extract ( const key_type & x ); template < class K > node_type extract ( const K & x ); [...]
iterator erase ( iterator position ); iterator erase ( const_iterator position ); size_type erase ( const key_type & x ); template < class K > size_type erase ( const K & x ); [...]
6. Revision history
6.1. R0 ➡ R1
Addressed feedback from the presentation on LEWG-I to align wording and approach with [P1690R1].