The std::transform-like function that returns transformed container
Simplest cases: matching container types
For the simple case where the input type matches the output type (which I've since realized is not what you're asking about) go one level higher. Instead of specifying the type T
that your container uses, and trying to specialize on a vector<T>
, etc., just specify the type of the container itself:
template <typename Container, typename Functor>
Container transform_container(const Container& c, Functor &&f)
{
Container ret;
std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
return ret;
}
More complexity: compatible value types
Since you want to try to change the item type stored by the container, you'll need to use a template template parameter, and modify the T
to that which the returned container uses.
template <
template <typename T, typename... Ts> class Container,
typename Functor,
typename T, // <-- This is the one we'll override in the return container
typename U = std::result_of<Functor(T)>::type,
typename... Ts
>
Container<U, Ts...> transform_container(const Container<T, Ts...>& c, Functor &&f)
{
Container<U, Ts...> ret;
std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
return ret;
}
What of incompatible value types?
This only gets us partway there. It works fine with a transform from signed
to unsigned
but, when resolving with T=int
and U=std::string
, and handling sets, it tries to instantiate std::set<std::string, std::less<int>, ...>
and thus doesn't compile.
To fix this, we want to take an arbitrary set of parameters and replace instances of T
with U
, even if they are the parameters to other template parameters. Thus std::set<int, std::less<int>>
should become std::set<std::string, std::less<std::string>>
, and so forth. This involves some custom template meta programming, as suggested by other answers.
Template metaprogramming to the rescue
Let's create a template, name it replace_type
, and have it convert T
to U
, and K<T>
to K<U>
. First let's handle the general case. If it's not a templated type, and it doesn't match T
, its type shall remain K
:
template <typename K, typename ...>
struct replace_type { using type = K; };
Then a specialization. If it's not a templated type, and it does match T
, its type shall become U
:
template <typename T, typename U>
struct replace_type<T, T, U> { using type = U; };
And finally a recursive step to handle parameters to templated types. For each type in a templated type's parameters, replace the types accordingly:
template <template <typename... Ks> class K, typename T, typename U, typename... Ks>
struct replace_type<K<Ks...>, T, U>
{
using type = K<typename replace_type<Ks, T, U>::type ...>;
};
And finally update transform_container
to use replace_type
:
template <
template <typename T, typename... Ts> class Container,
typename Functor,
typename T,
typename U = typename std::result_of<Functor(T)>::type,
typename... Ts,
typename Result = typename replace_type<Container<T, Ts...>, T, U>::type
>
Result transform_container(const Container<T, Ts...>& c, Functor &&f)
{
Result ret;
std::transform(std::begin(c), std::end(c), std::inserter(ret, std::end(ret)), f);
return ret;
}
Is this complete?
The problem with this approach is it is not necessarily safe. If you're converting from Container<MyCustomType>
to Container<SomethingElse>
, it's likely fine. But when converting from Container<builtin_type>
to Container<SomethingElse>
it's plausible that another template parameter shouldn't be converted from builtin_type
to SomethingElse
. Furthermore, alternate containers like std::map
or std::array
bring more problems to the party.
Handling std::map
and std::unordered_map
isn't too bad. The primary problem is that replace_type
needs to replace more types. Not only is there a T
-> U
replacement, but also a std::pair<T, T2>
-> std::pair<U, U2>
replacement. This increases the level of concern for unwanted type replacements as there's more than a single type in flight. That said, here's what I found to work; note that in testing I needed to specify the return type of the lambda function that transformed my map's pairs:
// map-like classes are harder. You have to replace both the key and the key-value pair types
// Give a base case replacing a pair type to resolve ambiguities introduced below
template <typename T1, typename T2, typename U1, typename U2>
struct replace_type<std::pair<T1, T2>, std::pair<T1, T2>, std::pair<U1, U2>>
{
using type = std::pair<U1, U2>;
};
// Now the extended case that replaces T1->U1 and pair<T1,T2> -> pair<T2,U2>
template <template <typename...> class K, typename T1, typename T2, typename U1, typename U2, typename... Ks>
struct replace_type<K<T1, T2, Ks...>, std::pair<const T1, T2>, std::pair<const U1, U2>>
{
using type = K<U1, U2,
typename replace_type<
typename replace_type<Ks, T1, U1>::type,
std::pair<const T1, T2>,
std::pair<const U1, U2>
>::type ...
>;
};
What about std::array?
Handling std::array
adds to the pain, as its template parameters cannot be deduced in the template above. As Jarod42 notes, this is due to its parameters including values instead of just types. I've gotten partway by adding specializations and introducing a helper contained_type
that extracts T
for me (side note, per Constructor this is better written as the much simpler typename Container::value_type
and works for all types I've discussed here). Even without the std::array
specializations this allows me to simplify my transform_container
template to the following (this may be a win even without support for std::array
):
template <typename T, size_t N, typename U>
struct replace_type<std::array<T, N>, T, U> { using type = std::array<U, N>; };
// contained_type<C>::type is T when C is vector<T, ...>, set<T, ...>, or std::array<T, N>.
// This is better written as typename C::value_type, but may be necessary for bad containers
template <typename T, typename...>
struct contained_type { };
template <template <typename ... Cs> class C, typename T, typename... Ts>
struct contained_type<C<T, Ts...>> { using type = T; };
template <typename T, size_t N>
struct contained_type<std::array<T, N>> { using type = T; };
template <
typename Container,
typename Functor,
typename T = typename contained_type<Container>::type,
typename U = typename std::result_of<Functor(T)>::type,
typename Result = typename replace_type<Container, T, U>::type
>
Result transform_container(const Container& c, Functor &&f)
{
// as above
}
However the current implementation of transform_container
uses std::inserter
which does not work with std::array
. While it's possible to make more specializations, I'm going to leave this as a template soup exercise for an interested reader. I would personally choose to live without support for std::array
in most cases.
View the cumulative live example
Full disclosure: while this approach was influenced by Ali's quoting of Kerrek SB's answer, I didn't manage to get that to work in Visual Studio 2013, so I built the above alternative myself. Many thanks to parts of Kerrek SB's original answer are still necessary, as well as to prodding and encouragement from Constructor and Jarod42.
Some remarks
The following method allows to transform containers of any type from the standard library (there is a problem with std::array
, see below). The only requirement for the container is that it should use default std::allocator
classes, std::less
, std::equal_to
and std::hash
function objects. So we have 3 groups of containers from the standard library:
Containers with one non-default template type parameter (type of value):
std::vector
,std::deque
,std::list
,std::forward_list
, [std::valarray
]std::queue
,std::priority_queue
,std::stack
std::set
,std::unordered_set
Containers with two non-default template type parameters (type of key and type of value):
std::map
,std::multi_map
,std::unordered_map
,std::unordered_multimap
Container with two non-default parameters: type parameter (type of value) and non-type parameter (size):
std::array
Implementation
convert_container
helper class convert types of known input container type (InputContainer
) and output value type (OutputType
) to the type of the output container(typename convert_container<InputContainer, Output>::type
):
template <class InputContainer, class OutputType>
struct convert_container;
// conversion for the first group of standard containers
template <template <class...> class C, class IT, class OT>
struct convert_container<C<IT>, OT>
{
using type = C<OT>;
};
// conversion for the second group of standard containers
template <template <class...> class C, class IK, class IT, class OK, class OT>
struct convert_container<C<IK, IT>, std::pair<OK, OT>>
{
using type = C<OK, OT>;
};
// conversion for the third group of standard containers
template
<
template <class, std::size_t> class C, std::size_t N, class IT, class OT
>
struct convert_container<C<IT, N>, OT>
{
using type = C<OT, N>;
};
template <typename C, typename T>
using convert_container_t = typename convert_container<C, T>::type;
transform_container
function implementation:
template
<
class InputContainer,
class Functor,
class InputType = typename InputContainer::value_type,
class OutputType = typename std::result_of<Functor(InputType)>::type,
class OutputContainer = convert_container_t<InputContainer, OutputType>
>
OutputContainer transform_container(const InputContainer& ic, Functor f)
{
OutputContainer oc;
std::transform(std::begin(ic), std::end(ic), std::inserter(oc, oc.end()), f);
return oc;
}
Example of use
See live example with the following conversions:
std::vector<int> -> std::vector<std::string>
,std::set<int> -> std::set<double>
,std::map<int, char> -> std::map<char, int>
.
Problems
std::array<int, 3> -> std::array<double, 3>
conversion doesn't compile because std::array
haven't insert
method which is needed due to std::inserter
). transform_container
function shouldn't also work for this reason with the following containers: std::forward_list
, std::queue
, std::priority_queue
, std::stack
, [std::valarray
].
Doing this in general is going to be pretty hard.
First, consider std::vector<T, Allocator=std::allocator<T>>
, and let's say your functor transforms T->U
. Not only do we have to map the first type argument, but really we ought to use Allocator<T>::rebind<U>
to get the second. This means we need to know the second argument is an allocator in the first place ... or we need some machinery to check it has a rebind
member template and use it.
Next, consider std::array<T, N>
. Here we need to know the second argument should be copied literally to our std::array<U, N>
. Perhaps we can take non-type parameters without change, rebind type parameters which have a rebind member template, and replace literal T
with U
?
Now, std::map<Key, T, Compare=std::less<Key>, Allocator=std::allocator<std::pair<Key,T>>>
. We should take Key
without change, replace T
with U
, take Compare
without change and rebind Allocator
to std::allocator<std::pair<Key, U>>
. That's a little more complicated.
So ... can you live without any of that flexibility? Are you happy to ignore associative containers and assume the default allocator is ok for your transformed output container?