In creating a code common for set
, unordered_set
, map
, and unordered_map
, I need the few methods, where the handling is actually different. My problem is getting the compiler to deduce, which implementation to use.
Consider the example:
#include <map>
#include <unordered_set>
#include <string>
#include <iostream>
using namespace std;
static unordered_set<string> quiet;
static map<const string, const string> noisy;
template <template <typename ...> class Set, typename K>
static void insert(Set<K> &store, const string &key, const string &)
{
cout << __PRETTY_FUNCTION__ << "(" << key << ")\n";
store.insert(key);
}
template <template <typename ...> class Map, typename K, typename V>
static void insert(Map<K, V> &store, const string &key, const string &v)
{
cout << __PRETTY_FUNCTION__ << "(" << key << ", " << v << ")\n";
store.insert(make_pair(key, v));
}
int
main(int, char **)
{
insert(noisy, "cat", "meow");
insert(quiet, "wallaby", ""); /* macropods have no vocal cords */
return 0;
}
Though the cat-line works, the wallaby-line triggers the following error from the compiler (clang-10):
t.cc:22:8: error: no matching member function for call to 'insert'
store.insert(make_pair(key, v));
~~~~~~^~~~~~
t.cc:29:2: note: in instantiation of function template specialization
'insert<unordered_set, std::__1::basic_string<char>, std::__1::hash<std::__1::basic_string<char> > >' requested here
insert(quiet, "wallaby", ""); /* macropods have no vocal cords */
The error makes it obvious, the quiet
, which is an unordered_set
, is routed to the insert-implementation for map
too -- instead of that made for the unordered_set
.
Now, this is not entirely hopeless -- if I:
template <template <typename ...> class Set, typename K, typename A, typename C>
static void insert(Set<K, A, C> &store, const string &key, const string &)
...
template <template <typename ...> class Map, typename K, typename V, typename A, typename C>
static void insert(Map<K, V, A, C> &store, const string &key, const string &v)
unordered_set
with set
.The program will compile and work as expected -- the compiler will distinguish set
from map
by the number of arguments each template takes (three vs. four).
But unordered_set
has the same number of arguments as map
(four)... And unordered_map
has five arguments, so it will not be routed to the map-handling method...
How can I tighten the set-handling function's declaration for both types of sets to be handled by it?
How can I handle both map
s and unordered_map
s in the same code?
You can use SFINAE techniques to basically say: consider this overload only when the insert
call inside is well-formed. E.g. something like this:
template <template <typename ...> class Set, typename K>
static auto insert(Set<K> &store, const string &key, const string &)
-> std::void_t<decltype(store.insert(key))>
{
cout << __PRETTY_FUNCTION__ << "(" << key << ")" << endl;
store.insert(key);
}
template <template <typename ...> class Map, typename K, typename V>
static auto insert(Map<K, V> &store, const string &key, const string &v)
-> std::void_t<decltype(store.insert(make_pair(key, v)))>
{
cout << __PRETTY_FUNCTION__ << "(" << key << ", " << v << ")" << endl;
store.insert(make_pair(key, v));
}
Demo
std::map
and std::unordered_map
both have mapped_type
member type and their set
counterparts don't. So, we can add some SFINAE with the help of std::void_t
:
template<template<typename...> class Map, typename K, typename V,
typename = std::void_t<typename Map<K, V>::mapped_type>>
void insert(Map<K, V>&, const string&, const string&) {
// ...
}
A more general solution if you need (and in your example you don't) to constraint both function templates:
template<class, typename = void>
struct is_map : std::false_type { };
template<class Map>
struct is_map<Map, std::void_t<typename Map::mapped_type>> : std::true_type { };
template<template<typename...> class Set, typename K,
std::enable_if_t<!is_map<Set<K>>::value, int> = 0>
void insert(Set<K>&, const string&, const string&) {
// ...
}
template<template <typename...> class Map, typename K, typename V,
std::enable_if_t<is_map<Map<K, V>>::value, int> = 0>
void insert(Map<K, V>&, const string&, const string&) {
// ...
}
C++11 solution:
template<class...> // or just <class> if genericity is not needed
struct std_void {
using type = void;
};
template<template<typename...> class Map, typename K, typename V,
typename = typename std_void<typename Map<K, V>::mapped_type>::type>
void insert(Map<K, V>&, const string&, const string&) {
// ...
}
Note added. The code in the question was targeted at GCC 4.4.7. This is a pretty old GCC version, which doesn't fully support C++11 standard. In particular, it doesn't support using
type aliases, so std_void
should be implemented via old-fashioned typedef
:
template<class...>
struct std_void {
typedef void type;
};
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With