std::unordered_map
is a highly configurable class template with five parameters:
template< class Key, class Value, class Hash = std::hash<Key>, class KeyEqual = std::equal_to<Key>, class Allocator = std::allocator< std::pair<const Key, Value> > > class unordered_map;
Typical usage depends on default values for most of these parameters:
using my_map=std::unordered_map<int,std::string>;
but things get cumbersome when we want to specify one of the usually defaulted types:
template<typename T> class my_allocator{ ... };
using my_map=std::unordered_map<
int, std::string,
std::hash<int>, std::equal_to<int>,
my_allocator< std::pair<const int, std::string> >
>;
In the example, we are forced to specify the hash and equality predicate with their default value types just to get to the allocator, which is the parameter we really wanted to specify. Ideally we would like to have a syntax like this:
// this is not actual C++
using my_map = std::unordered_map<
Key=int, Value=std::string,
Allocator=my_allocator< std::pair<const int, std::string> >
>;
Turns out we can emulate this by resorting to designated initializers, introduced in C++20:
template<
typename Key, typename Value,
typename Hash = std::hash<Key>,
typename Equal = std::equal_to<Key>,
typename Allocator = std::allocator< std::pair<const Key,Value> >
>
struct unordered_map_config
{
Key *key = nullptr;
Value *value = nullptr;
Hash *hash = nullptr;
Equal *equal = nullptr;
Allocator *allocator = nullptr;
using type = std::unordered_map<Key,Value,Hash,Equal,Allocator>;
};
template<typename T>
constexpr T *type = nullptr;
template<unordered_map_config Cfg>
using unordered_map = typename decltype(Cfg)::type;
...
using my_map = unordered_map<{
.key = type<int>, .value = type<std::string>,
.allocator = type< my_allocator< std::pair<const int, std::string > > >
}>;
The approach taken by the simulation is to use designated initializers to create an aggregate object consisting of dummy null pointers: the values of the pointers do not matter, but their types are captured via CTAD and used to synthesize the associated std::unordered_map
instantiation. Two more C++20 features this technique depends on are:
- Non-type template parameters have been extended to accept literal types (which include aggregate types such as
unordered_map_config
instantiations). - The class template
unordered_map_config
can be specified as a non-type template parameter ofunordered_map
. In C++17, we would have had to defineunordered_map
as
template<auto Cfg>
which would force the user to explicit name
using unordered_map = typename decltype(Cfg)::type;unordered_map_config
inusing my_map = unordered_map<
unordered_map_config
{...}>;
There is still the unavoidable noise of having to use the type
template alias since, of course, aggregate initialization is about values rather than types.
Another limitation of this simulation is that we cannot mix named and unnamed parameters:
// compiler error: either all initializer clauses should be designated
// or none of them should be
using my_map = unordered_map<{
type<int>, type<std::string>,
.allocator = type< my_allocator< std::pair<const int, std::string > > >
}>;
C++20 designated parameters are more restrictive than their C99 counterpart; some of the constraints (initializers cannot be specified out of order) are totally valid in the context of C++, but I personally fail to see why mixing named and unnamed parameters would pose any problem.