Reputation: 2164
I need to make a function that should be as generic as possible, suppose I have a couple of map
s to work with --
int main()
{
map<int, string> m1 = {{0, "abc"}, {1, "def"}, {2, "ghi"}} ;
map<int, double> m2 = {{0, 0.5}, {1, 0.6}, {2, 0.7}} ;
map<int, vector<string>> m3 = {{0, {"abc", "def"}},
{1, {"ghi", "ijk"}}};
dosomething(m1);
dosomething(m2);
}
and my template
d dosomething()
function looks like this --
template <typename A, typename B>
void dosomething(map<A,B> m)
{
for(auto e : m)
{
// do something with e
}
}
Now if I want to write a function that works with any kind of map
(i.e. map<int, vector<string>
or map<int, string>
or map<int, double>
) how do I do that ? It would be really nice if I could do like something this --
template <typename A, typename B>
void dosomething(map<A,B> m)
{
for(auto me: m)
{
if (the type of B is not a standard container type)
{
// do something with me.second
}
else if(the type of B is a something from the standard container)
{
for(int i = 0 ; i < (me.second).size() ; i++)
// do something with me.second[i]
}
}
}
How do I do that in c++ ? I am assuming the compiler follows c++11 specs.
Upvotes: 0
Views: 415
Reputation: 153840
The code in template instantiations is meant to compile. As a result, when doing different things depending on the types of template arguments, you typically can't have them in one function. Instead, you'd delegate the processing to a suitably overloaded function which itself may be a template made conditionally applicable. For example, your dosomething()
function could look like this:
// suitable declaration/definition of dosomething_apply() go here; see below
template <typename A, typename B>
void dosomething(std::map<A, B> const& m) {
for (auto&& me: m) {
dosomething_apply(me.second);
}
}
As an aside: do not use for (auto e: m)
unless you have a very good reason why you need this form! In the majority of the cases it is a performance problem. Likewise, you shouldn't pass bigger objects by value but rather pass them using a suitable reference type.
The slightly tricky business is determining whether the argument to dosomething_apply()
is a standard container or not: there is certainly no type trait which classifies all standard as such. It is possible to create a corresponding type trait, though. The next question is whether this is actually what you want to detect because your supposed layout using indices on the element implies that me.second
has an index-based subscript operator which is true only for std::vector<...>
, std::dequeue<...>
, std::basic_string<...>
, std::array<...>
, and std::bitset<...>
(not sure if this is the complete set of containers providing subscripts). There are other containers which do not subscript operations, e.g. std::list<...>
, std::map<...>
, std::unordered_map<...>
, etc.
All of these containers (except std::bitset<...>
) provide an iterator interface, though. It may be more reasonable to detect whether a type provide an iterator interface and use this interface in the implementation of dosomething_apply()
. Since you seem to be set to use index-based subscripts, the example below deals with the set of class templates noted above, though.
The first step is creating a suitable traits class which detects the desired types asking for special handling. In many cases presence of suitable member functions or member types can be detected. Since you specifically asked for standard types, it is necessary to list the supported types, though, as member functions or member types could also be detected for non-standard classes. Here is an example traits class called is_subscripted
:
template <typename T>
struct is_subscripted: std::false_type {};
template <typename T, std::size_t N>
struct is_subscripted<std::array<T, N>>: std::true_type {};
template <std::size_t N>
struct is_subscripted<std::bitset<N>>: std::true_type {};
template <typename cT, typename Al>
struct is_subscripted<std::deque<cT, Al>>: std::true_type {};
template <typename cT, typename Tr, typename Al>
struct is_subscripted<std::basic_string<cT, Tr, Al>>: std::true_type {};
template <typename cT, typename Al>
struct is_subscripted<std::vector<cT, Al>>: std::true_type {};
It simply creates a default version of trait which states that trait isn't matched by deriving from std::false_type
. The corresponding template is then specialized for the class templates listed above, each one rather deriving from std::true_type
. This way an expression of the form is_specialized<T>::value
can be used to detect if the trait applies to the type T
.
The next step is providing suitable handler functions. Since the trait is either present or absent, using enable_if_t<...>
is a simple way to go:
template <typename T>
std::enable_if_t<!is_subscripted<T>::value>
dosomething_apply(T const& value) {
std::cout << "value=" << value << '\n';
}
template <typename T>
std::enable_if_t<is_subscripted<T>::value>
dosomething_apply(T const& range) {
for (auto size(range.size()), i(size - size); i != size; ++i) {
std::cout << "range=" << range[i] << '\n';
}
}
Using std::enable_if_t<Value>
with a Value
evaluating to true
yields void
(using a second argument, e.g., std::enable_if_t<Value, double>
to get a different type) and the function template becomes applicable. Using it with a Value
evaluating to false
doesn't yield a type and the function template is ignored (see SFINAE for an explanation of what's going on).
... and that's it really. All it takes to put together a suitable program and things should work. Below is a complete program ready to be fed to a C++14 compiler. There are some minor uses of C++14 in the code above, e.g. using std::enable_if_v<T>
instead of typename std::enable_if<T>::type
. It should be relatively straight forward to replace the C++14 usage with C++11 usage if you need to pass the code through a compiler for the previous C++ standard.
#include <map>
#include <iostream>
#include <utility>
#include <array>
#include <bitset>
#include <deque>
#include <string>
#include <vector>
// ----------------------------------------------------------------------------
template <typename T>
struct is_subscripted: std::false_type {};
template <typename T, std::size_t N>
struct is_subscripted<std::array<T, N>>: std::true_type {};
template <std::size_t N>
struct is_subscripted<std::bitset<N>>: std::true_type {};
template <typename cT, typename Al>
struct is_subscripted<std::deque<cT, Al>>: std::true_type {};
template <typename cT, typename Tr, typename Al>
struct is_subscripted<std::basic_string<cT, Tr, Al>>: std::true_type {};
template <typename cT, typename Al>
struct is_subscripted<std::vector<cT, Al>>: std::true_type {};
// ----------------------------------------------------------------------------
template <typename T>
std::enable_if_t<!is_subscripted<T>::value>
dosomething_apply(T const& value) {
std::cout << "value=" << value << '\n';
}
template <typename T>
std::enable_if_t<is_subscripted<T>::value>
dosomething_apply(T const& range) {
for (auto size(range.size()), i(size - size); i != size; ++i) {
std::cout << "range=" << range[i] << '\n';
}
}
// ----------------------------------------------------------------------------
template <typename A, typename B>
void dosomething(std::map<A, B> const& m)
{
for (auto&& me: m) {
dosomething_apply(me.second);
}
}
int main()
{
dosomething(std::map<int, int>{ { 1, 2 }, {3, 4 } });
dosomething(std::map<int, std::array<int, 2> >{ { 1, { { 2, 3 }} }, {4, { { 5, 6 } } } });
dosomething(std::map<int, std::bitset<4> >{ { 1, std::bitset<4>("1010") }, {2, std::bitset<4>("0011") } });
dosomething(std::map<int, std::deque<int>>{ { 10, { { 12, 13, 14 } } }, { 20, { 22, 23, 24 } } });
dosomething(std::map<int, std::string>{ { 1, "one" }, {2, "two" } });
dosomething(std::map<int, std::vector<int>>{ { 30, { { 32, 33, 34 } } }, { 40, { 42, 43, 44 } } });
}
Upvotes: 3