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.