4

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:

  1. Spell out all of the template-parameters -- including the optional ones (comparator, allocator, etc.)
    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)
    
  2. Replace the 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 maps and unordered_maps in the same code?

HTNW
  • 27,182
  • 1
  • 32
  • 60
Mikhail T.
  • 3,043
  • 3
  • 29
  • 46
  • `if constexpr` with traits? – Dmitry Kuzminov Dec 28 '20 at 04:42
  • Do you need the third unused parameter for the sets? – Dmitry Kuzminov Dec 28 '20 at 04:44
  • `constexpr` is not available in g++-4.4.7, which is the stock compiler on RedHat-6 used by our production systems. The third parameter is there for uniformity -- in my real code I'm calling the `insert` from a template common for all container-types (maps and sets, ordered and otherwise). – Mikhail T. Dec 29 '20 at 03:03
  • What is the purpose of this "uniformity"? I guess that the goal was to differenciate, not to make it uniform. The dummy parameter smells. – Dmitry Kuzminov Dec 29 '20 at 04:06
  • "g++-4.4.7," "production" This leads me to the question why a company stays years behind the presence. There may be fears against using C++20 with today's compilers, but sitting 12 years in the past...? There is more than a technical problem with that company I believe. – Klaus Jan 10 '21 at 09:34
  • Because RedHat-6.x is still supported (by RedHat), and g++-4.4.7 is the stock compiler on it. Although newer machines here are RedHat-7.x, the older ones are still running... Yes, I can -- and have -- build a newer compiler by hand, but that, unfortunately, creates other issues... – Mikhail T. Jan 11 '21 at 02:10

4 Answers4

4

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

HTNW
  • 27,182
  • 1
  • 32
  • 60
Igor Tandetnik
  • 50,461
  • 4
  • 56
  • 85
  • 1
    In C++20, we can also do `template – HTNW Dec 28 '20 at 06:02
  • This looks perfect - as it goes to the heart of the problem: I want to distinguish classes based on the arguments their `insert`-method takes. And it works with clang10, which I use for development, because it has superior diagnostics. Unfortunately, I still need to compile it with g++-4.4.x (the base compiler on REDHAT6), which chokes on this construct even with `-std=gnu++0x` :-( – Mikhail T. Dec 28 '20 at 16:20
  • @MikhailT., `std::void_t` is not a C++11 feature, but it can be implemented manually. – Evg Dec 28 '20 at 19:07
2

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;
};
Evg
  • 25,259
  • 5
  • 41
  • 83
  • Thank you. Will this work with the (incomplete) c++11 implementation of g++ version 4.4.x? My actual program still needs to build on RedHat-6... – Mikhail T. Dec 28 '20 at 16:22
  • @MikhailT., as written, it uses C++17 languages features. But nothing here is specifically C++17. `std::enable_if_t` is just `typename std::enable_if::type` and `std::void_t` is `typename my_void::type` with `template struct my_void { using type = void; }`. – Evg Dec 28 '20 at 19:06
  • I'm sorry, @Evg, but I do not understand your last comment. Could you edit your answer to provide a working sample for g++ 4.4.7? Defining `foo_void_t` first and then using it? Just the first part will suffice - no need for the more elaborate "general solution". Thanks again! – Mikhail T. Dec 28 '20 at 21:29
  • I think, you have an extra `typename` in there -- to the right of the `=`. If I remove it, the snippet compiles with the newer compilers. But g++-4.4.7 fails on the `std_void`-construct: `error: expected nested-name-specificier before 'type'` – Mikhail T. Dec 29 '20 at 01:58
  • So I replaced the `using type` with a `typedef void type;` and the rest of it worked! If you modify your answer once again, I'll finally have one to "accept". Thank you for your patience! – Mikhail T. Dec 29 '20 at 02:04
  • @MikhailT., it seems that GCC 4.4.7 doesn't implement all C+11 features and in particular it doesn't support `using` for defining type aliases. – Evg Dec 29 '20 at 06:01
  • Yes, and, as I wrote, I got it to work by replacing your `using type = void` with `typedef void type`... – Mikhail T. Dec 31 '20 at 19:17
  • I fixed your C++11 version -- so I could accept your answer. Why did you revert my edit? – Mikhail T. Jan 10 '21 at 01:49
  • @MikhailT. Because my code is already valid C++11 code. The only reason for the fix is the old GCC compiler that doesn't fully implement C++11 standard. The idea of SO is that answers to questions from particular users could be useful to other people. Suppose someone later finds your question and my answer. Why should he/she be bothered by the fact that some old compiler doesn't speak C++11, while the question is not about a particular compiler (GCC version is not even mentioned in the question), but template metaprogramming in general? I added a note into the answer. – Evg Jan 10 '21 at 07:49
  • The version I created works with newer compilers too -- tested with both g++-8 and clang-10... – Mikhail T. Jan 11 '21 at 02:08
  • @MikhailT. It works, but using `typedef` is now discouraged by many style guidelines. `using` is more readable and supports `template`. – Evg Jan 11 '21 at 07:39
0

First of all, the actual solution, that works with compiler-versions currently found in the wild -- tested with gcc-4.4.7 (stock compiler on RedHat6), gcc-8.3.1, and clang-10 is:

template<class...>  // or just <class> if genericity is not needed
struct std_void {
    typedef void type;
};

template<template<typename...> class Map, typename K, typename V,
         typename = std_void<typename Map<K, V>::mapped_type>>
void insert(Map<K, V>& store, const string &key, const string &value) {
    // ...
}

To a reader from the (glorious) future, by all means, use the techniques proposed by @Evg (on whose proposal this one is based) or the one by @Igor-Tandetnik -- if your compiler supports the necessary features.

Evg's answer -- and this one derived from it -- select the template based on whether or not its type defines a mapped_type. Igor's differentiates on the arguments taken by the template's insert-method. Either would've been suitable to my purposes, but Evg's was easier to adapt to the older compiler.


In closing, I must vent my frustration with the state of C++: this shouldn't be so difficult. I got it to work, but a colleague trying to read my code will not understand it -- not without multiple passes through it and numerous curses and "gotchas!"

Yes, the C++ programs are compiled (into binary, rather than "byte", code), and the run-time type information (RTTI) necessary for the full "reflection" is (very) expensive.

But what I needed does not require run time reflection! All of the information necessary is known at compile time, so why don't I have all the necessary language-features already, three decades after it was first introduced? Even 10 years is way too long a wait for such functionality -- chip-manufacturers make processors 32-times fatter during the same period.

The constexpr is sort of it, thank you kindly, but it was added so recently, my compiler still has no support for it...

Mikhail T.
  • 3,043
  • 3
  • 29
  • 46
-3

You must use partial template specializations. Like this:

template<template<typename... > class Map, typename T, typename U> static void
insert (Map<T,U>&, const T&, const U&); // generic function
template<typename T, typename U> static void
insert<std::map<T,U>, T, U> (std::map<T,U>&, const T&, const U&); // this is only called if Map is std::map
// you can put more specializations here

The first function is a generic function, which will be called for all Map except if the type Map doesn't appear in any template specialization for the same function. The second function is the same as the first but it's body is only excecuted if the type Map is std::map<T,U>.

See the wikipedia article on partial template specialization

  • 2
    There ain't no such thing as a partial specialization of a function template. Only class templates can be partially specialized. The very article you cite states this. Functions you show are two unrelated function templates overloading the same name. – Igor Tandetnik Dec 28 '20 at 04:55