1

In short:

  • it is possible to write a C++20 function that accepts as argument a map, unordered_map or any other (present or future) implementation of a map ?

  • if I want restrict some parameter of a template to values with the behavior of a map (map, unordered_map or any other that appears), which C++20 concept must I use?

Long explanation:

When switching from one programming language to another, it is usual that you miss some features of the previous one in the next one. Usually that does means one language better than another (please, no answers nor comments to start a language war) , but is the result of design decisions in the languages and personal preferences of the coder.

In particular, when switching from Java to C++, I miss the strongly structured schema of classes of Java. By example, there are one interface Map that defines what is expected of a Map, and there are the different implementations (HashMap, TreeMap, ...) of it, optimized to some scenarios. In this way, the user can create code independent of the implementation (except when constructing the container). In C++, the are two usual map templates, "map" and "unordered_map":

template <typename _Key, typename _Tp, typename _Compare = std::less<_Key>,
        typename _Alloc = std::allocator<std::pair<const _Key, _Tp> > >
    class map
    { ...


template<typename _Key, typename _Tp,
       typename _Hash = hash<_Key>,
       typename _Pred = equal_to<_Key>,
       typename _Alloc = allocator<std::pair<const _Key, _Tp>>>
    class unordered_map
    { ...

as you can see, they do not share any common base class, thus, it is not possible (?) define an user function that accepts both kind of maps.

In addition, C++20 templates allows "concepts" to restrict the template parameters. How to write a concept equivalent to Java "<T implements Map<String,Integer>" ?. It seems necessary reimplement as concepts near than all class interfaces (?).

pasaba por aqui
  • 3,446
  • 16
  • 40
  • You can use concept to check whether `T` has map-representative member types such as `key_type` and `mapped_type`, and related operations such as `m.upper_bound()` and `m.lower_bound()`. – 康桓瑋 Oct 31 '21 at 11:35
  • It’s true that it’s not possible to write a **function** that accepts both kinds, but that’s not the point in C++, where one expects to write a function **template** that does so (and avoids dynamic dispatch overhead without any JIT). Note that, even without any `implements Map`-like information, almost all failures are caught at compile time, which is much of the reason that C++ has never bothered with anything but hacky and/or approximate means of providing it. – Davis Herring Nov 07 '21 at 01:01
  • It is possible to write a family of classes that bind as a reference to a map-like class and provide a runtime-polymorphic interface, allowing you to have a function that takes a `map_view`. There are places for it, but it comes at a runtime cost. I’ve prototyped it, using Boost’s basic_any to hold the iterators, but haven’t used it in production. It does seem useful. – Ben Nov 10 '21 at 03:05

3 Answers3

4

I assume that by saying:

I want restrict some parameter of a template to values with the behavior of a map

you mean that you want a template type parameter to be a map-like type. I can actually answer both parts of this question with one function prototype (see doMapStuff below). First, you need to check if a given type is a "map" or "map-like".

Checking the argument type

Normally, you need first some sort of interface to check if a member has specific common members. But because we're just checking if a type is "map-like", we can just check member types. In the case of map-like types, a simple way would be to test the value_type, key_type, and mapped_type member types.This can easily be done even without C++20
Starting with C++11, we can use std::is_same<T> with a static assert like this:

template<class MapTy>
struct is_map_type : std::is_same<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>> {};

Or, in C++17 and onwards you can even do this (using C++17's std::is_same_v helper):

template<class MapTy>
constexpr bool is_map_type_v = std::is_same_v<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>

What these do is essentially check if the template type's value_type (which exists for many different collection types) is exactly astd::pair of the type's const key_type and mapped_type. This is true and thus will check for "any" map-like type, including std::multimap and std::unordered_multimap. There are multiple ways to implement is_map_type, even Pre-C++11, some of which can be seen in this StackOverflow question (my code snippet above was partially taken from that question).

However, if you specifically want to use C++20 concepts, it's also very simple to do so if you want to implement it the same way:

template<class MapTy>
concept is_map_type = std::is_same_v<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>

Or if you want to use C++20's built-in std::same_as concept:

template<class MapTy>
concept is_map_type = std::same_as<typename MapTy::value_type, std::pair<const typename MapTy::key_type, typename MapTy::mapped_type>>;

The actual function

it is possible to write a C++20 function that accepts as argument a map, unsorted_map or any other (present or future) implementation of a map ?

if I want restrict some parameter of a template to values with the behavior of a map (map, unsorted_map or any other that appears), which C++20 concept must I use?

You can actually do both with one function, like I said, both with and without C++20.
In C++11 onward:

template <class MapTy>
void doMapStuff(const MapTy& m) { //function argument doesn't have to be const reference, I'm just doing that in the example
    static_assert(is_map_type<MapTy>::value, "Passed map type isn't a valid map!");
    //Or, if using the C++17 onward version:
    static_assert(is_map_type_v<MapTy>, "Passed map type isn't a valid map!");
    //Do something with argument 'm'...
}

Or, using C++20 requires with the C++20 concept we made:

template <class MapTy>
requires is_map_ty<MapTy>
void doMapStuff(const MapTy& m) {
    //...
}

Basically, in either version the function will make sure that the passed template parameter will always satisfy the requirements we set for our map-like type (which in this case checks value_type, key_type, and mappped_type like I said). Otherwise, the program won't compile; In the case of the static_assert version, you'll get the message string and a compilation error during compiling. With the C++20 concept version, you'' get a compilation error saying that the arguments passed to the doMapStuff are of incorrect type.

Since the template type is used as the type of the first argument, you can use the function like this (without having to explicitly specify the type separately):

std::map<std::string, int> myMap;
doMapStuff(myMap);
//...
std::vector<int> myVec;
doMapStuff(myVec); //compile error! MapTy = std::vector<int>

Edit

It seems based on what you have said in the comments that you only want to check if value_type is any pair, which a bit different from before. We first need a type trait to check if a template type is a pair. This is pretty much the same regardless of the C++ version:

template <typename>
struct is_pair : std::false_type { };

template <typename K, typename V>
struct is_pair<std::pair<K, V>> : std::true_type { };

This will create a bool_cosntant struct we can use to check if a passed type is a valid pair. We then can define our is_map type trait similarly to before.
In C++11 onward:

template<class MapTy>
struct is_map : is_pair<typename MapTy::value_type> {};

//C++17 compliant option
template<class MapTy>
constexpr bool is_map_v = is_pair<typename MapTy::value_type>::value;

In C++20 using concepts:

template<class MapTy>
concept is_map_like = is_pair<typename MapTy::value_type>::value;

The usage in functions is pretty much the same; we once again don't have to explicitly pass a template type - it's deduced from the passed argument.
In C++11:

template <class MapTy>
void doMapStuff(const MapTy& m) {
    static_assert(is_map_v<MapTy>, "Passed map type doesn't have a valid pair!");
    //OR...
    static_assert(is_map<MapTy>::value, "Passed map type doesn't have a valid pair!");
}

In C++20 using concepts:

template <class MapTy>
requires is_map_like<MapTy>
void doMapStuff(const MapTy& m) {
    //...
}

Although, it is important to note that the mapped_type alias is unique to std "map-like"s, unlike value_type; The original is_map_type checks both. See this SO question for more details.

Abob
  • 751
  • 5
  • 27
  • Nice and practical statements, thanks. However, the problem with this kind of solutions is we are coding that something is map iff it has a typedef value_type that is a pair. Something that can be give false positives (something with a value_type pair that is not a map) and false negatives (something that is a map but has not a typedef value_type or names it in aother way). – pasaba por aqui Nov 10 '21 at 11:12
  • 2
    @pasabaporaqui you get to define what is and what isn't a map. Only the maps in `std` define a `mapped_type` so that's a pretty good test. Nitpick: in `std` traits are named `is_foo` and concepts are named `foo`, so I'd name these `is_mapping` and `mapping` or similar – Caleth Nov 10 '21 at 11:20
  • @pasabaporaqui I agree with what Caleth says. You can choose what to test for what is and isn't a "map" in your implementation. I just chose what I did because only map types have a `std::pair` value type, and like Caleth said, having a `mapped_type` is pretty much unique to types. If it doesn't have a `mapped_type`, then it probably isn't a map, or it's named something else because it's user defined. In the user defined case, you can either inform them to change it so that their map type has a `mapped_type`, or check for multiple names in your `is_map_type` type trait. – Abob Nov 10 '21 at 16:53
  • @pasabaporaqui I have edited my answer to add a section that hopefully answers your specific needs better. – Abob Nov 10 '21 at 17:39
0

Java has its own way to implement generics, and for that in C++ we have templates. Therefore, the answer to your first question is "just use a template".

But it's important to understand that when you define a templated function you are not defining an actual function, you are defining a "recipe" that the compiler can use to create a function. The compiler will create a different function (a template instantiation, if you will) for every different parameter type that is used in the template. This is all done at compile type and thus with templates you get static polymorphism.

Consider the following code

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);

    return 0;
}

Running this program produces

Map:
  one: 1
  three: 3
  two: 2
Map:
  thirty: 30
  twenty: 20
  ten: 10

This will work for any type as long as the type passed to print can be iterated with a range for, and each "element" can be decomposed into two values using structured binding. If you use some method more specific to a map, for instance insert, then the type provided to the print function must also have that method defined.

Now let's confirm that indeed we have two different functions in the binary. Suppose the generated binary name is "main", we can inspect the generated binary using the nm program in Linux to see search for functions named "print" in the binary.

With

nm -C main | grep print

we get something like

00000000000033f1 W void print<std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > >(std::unordered_map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::hash<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::equal_to<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > const&)
00000000000032b3 W void print<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > >(std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double, std::less<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > >, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, double> > > const&)

The output is a bit ugly, but we can see that we get two completely independent functions. If we add a std::unordered_map<std::string, int> variable and use the print function to print it, we will get another implementation of print, since that is a different type.

What if we want to print a std::vector? A vector supports range for (any type that has a begin and end methods returning iterators will work), but if each element in the vector cannot be decomposed into two values with structured binding, then it will not work and we get a compile error. That means something like std::vector<std::pair<double, double>> will work, but std::vector<double> won't.

But our print function prints "Map" in the beginning it could be better if it didn't match std::vector<std::pair<double, double>> at all. That comes to your second question. Templates are "too flexible" and that can cause problems (including hard to understand error messages). Sometimes we want to reduce that flexibility.

To illustrate this, let's try to use print with a std::vector<double>.

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;
    std::vector<double> v{1, 2, 3};

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);
    print(v);

    return 0;
}

If we try to compile this we get an error like

<path>/main.cpp: In instantiation of ‘void print(const T&) [with T = std::vector<double>]’:
<path>/main.cpp:47:10:   required from here
<path>/main.cpp:11:21: error: cannot decompose non-array non-class type ‘const double’
   11 |     for(const auto& [key, value] : map) {
      |                     ^~~~~~~~~~~~

We can define a separated print function for std::vector<T>. For instance, running the code below

#include <iostream>
#include <map>
#include <string>
#include <unordered_map>
#include <vector>

template <typename T>
void print(const T& map) {
    std::cout << "Map:\n";

    for(const auto& [key, value] : map) {
        std::cout << "  " << key << ": " << value << "\n";
    }
}

template <typename T>
void print(const std::vector<T>& v) {
    std::cout << "v: [";
    for (const auto& elem : v) {
        std::cout << elem << ", ";
    }
    std::cout << "]\n";
}

int main(int argc, char* argv[]) {
    std::map<std::string, double> m;
    std::unordered_map<std::string, double> m2;
    std::vector<double> v{1, 2, 3};

    m.insert({"one", 1});
    m.insert({"two", 2});
    m.insert({"three", 3});

    m2.insert({"ten", 10});
    m2.insert({"twenty", 20});
    m2.insert({"thirty", 30});

    print(m);
    print(m2);
    print(v);

    return 0;
}

results in

Map:
  one: 1
  three: 3
  two: 2
Map:
  thirty: 30
  twenty: 20
  ten: 10
v: [1, 2, 3, ]

That is good, but what if we want our print function for vectors to work with anything that behaves like a vector? If we use just void print(const T& v) for our "vector-like" print function we get a compile error due to a redefinition of print. We have to limit each print function to work with disjoints "sets of types", each obeying some condition.

Previously to c++20 your option was using type traits with static assert (previous to C++17) or with if constexpr. With C++20 you get a better (simpler) way using concepts.

Arastais's answer covers this and I'll just add a few comments. Requiring existence of value_type and key_type is perfectly fine and any third party "map-like" class is encouraged to implement these aliases as a way to work with generic code (your templates) that were created with the STL containers in mind. That is why the STL containers have these aliases1 in the first hand, to make it easier to write generic code. But, it is possible that some third map types does not have these aliases while still having a "map-like" interface2 as far as your use of that "map-like" type is concerned about. In that case you might consider using the existence of the actual used member functions as the condition for accepting a template. This is where C++20 concepts really shine. It's really easy to define such concepts either combining existing concepts or using requires expressions.

Footnotes

1 See the "Member types" section in cppreference for stl types, such as vector, unordered_map, set, etc.

2 Maybe all you need is the insert method, or accessing a value using something lime mymap[key], for instance. If that is enough you can use that as your "map interface" when defining the conditions.

darcamo
  • 3,294
  • 1
  • 16
  • 27
0

I recently wanted to write something similar and ended up with an solution like the following, which may be helpful. What I wanted was a class template with two type parameters, for a value-like type and a map-like type using the former for its keys and values:

template<std::regular T, std::semiregular Data = std::unordered_map<T,T>>
    requires std::ranges::input_range<Data>
    && requires (Data d, T t) {
        { d.clear() };
        { d.emplace(t, t) };
        { d[t] } -> std::same_as<T&>;
    }
class Foo
{
    Data m_data{};

    // Example of overloading with additional constraints. The compiler
    // selects the most constrained signature.
    void reserve(auto) {}

    void reserve(auto n) requires std::ranges::sized_range<Data> &&
        requires (Data d, std::ranges::range_size_t<Data> n)
        { d.reserve(n); } 
    {
        // Functionality not supported by std::map
        m_data.reserve(n);
    }

public:
    // Example of a function that may be absent entirely
    friend bool operator==(Foo const&, Foo const&)
        requires std::equality_comparable<Data>
    = default;
};

The thought process I recommend for this new style of C++ programming is like this:

  • Formulate the bare minimum your class/function template needs to know about its type parameters, in order for it to compile and function correctly;
  • Use these as the constraints for the parameters of your base class or function, knowing that
  • You can always fine tune whatever functionality you provide through additional, stricter constraints. In addition to my two examples, you can also use if constexpr to create local, conditionally compiled sections in your code.

Obviously, if you're going to use similar requires clauses in many places, it also makes sense to refactor those into separate concepts. The templates you write will typically use only a subset of the functionality their type parameters may have, that's why concepts tend to be fine-grained and hierarchic, like interfaces are in Java. This also helps make the error messages more readable if a constraint can cause compilation to fail.

sigma
  • 2,758
  • 1
  • 14
  • 18
  • Forgot to mention that my (main) motivation for this is another complication of C++: the standard container templates have more parameters than just their value types, and a different combination of types means a different, unrelated container class. So if these attributes are not relevant to your task, the easiest way is to just write a template for a container-like, non-template type parameter, and leave the rest to the caller. – sigma Jun 09 '22 at 15:44