Suppose you want to write a function that takes any iterable and converts it to a std::vector
. This is sort of like the C# analogue to the .ToList()
LINQ method. Here’s a starter kit:
template<typename Container> auto to_vector(Container&& c) { using ElementType = std::decay_t<decltype(*c.begin())>; std::vector<ElementType> v; std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
This deduces the underlying value type of the vector from the original container, and it uses the default allocator. The std::
does multiple things for us here: It removes the references from the dereferenced iterator, and it also removes the const
and volatile
attributes. That second step allows a container of const T
to convert to a vector of T
.
But maybe you want to let the caller override the underlying value type. For example, given a std::list<int>
, you want to produce a std::vector<long>
by saying
std::list<int> l = /* some expression */; auto v = to_vector<long>(l);
Okay, so you add an ElementType
type parameter to the template function, and have it default to the container’s underlying type.
template<typename Container, typename ElementType = std::decay_t<decltype(*std::declval<Container>().begin())>> auto to_vector(Container&& c) { std::vector<ElementType> v; std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
This works great, but using it is an annoyance because in order to customize the ElementÂType
, you have to restate the Container
first.
std::list<int> l = /* some expression */; auto v = to_vector<std::list<int>&, long>(l);
You’d much rather say
std::list<int> l = /* some expression */;
auto v = to_vector<long>(l);
This reads much nicer because it looks like you’re saying “I’m converting to vector<long>
.” So you swap the order of the type parameters:
template< typename ElementType = std::decay_t<decltype(*std::declval<Container>().begin())>, typename Container> auto to_vector(Container&& c) { std::vector<ElementType> v; std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
Unfortunately, this doesn’t compile because you are trying to default ElementType
based on a Container
that hasn’t been declared yet.
So how can you get a template type parameter to default to a value that is dependent upon a future type parameter?
You use a trick.
template< typename ElementType = void, typename Container> auto to_vector(Container&& c) { using ActualElementType = std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t<decltype(*c.begin())>, ElementType>; std::vector<ActualElementType> v; std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
The formal type parameter ElementType
defaults to void
, which is a sentinel value that means “Substitute the underlying value type of the collection here.” Inside the function body, after we have successfully deduced the Container
, we calculate the ActualÂElementÂType
by using the explicitly-provided ElementÂType
or (if it was defaulted to void
) using the underlying type of the Container
.
Adding support for custom allocators is more complicated because the allocator is a parameter. We have to convert the void
to an actual allocator by the time we get to the parameter list.
template< typename ElementType = void, typename Allocator = void, typename Container> auto to_vector(Container&& c, std::conditional_t< std::is_same_v<Allocator, void>, std::allocator< std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t<decltype(*std::declval<Container>().begin())>, ElementType>>, Allocator> al = {}) { using ActualElementType = std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t<decltype(*std::declval<Container>().begin())>, ElementType>; using ActualAllocator = std::conditional_t< std::is_same_v<Allocator, void>, std::allocator<ActualElementType>, Allocator>; std::vector<ActualElementType, ActualAllocator> v(al); std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
There’s a lot of repetition here: We want to use ActualAllocator
defined in the function body, but we can’t use it in the function prototype, so we have to inline its definition, producing a big ugly mess.
One way to solve this is to “save” it in another defaulted template type parameter.
template< typename ElementType = void, typename Allocator = void, typename Container, typename ActualElementType = std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t<decltype(*std::declval<Container>().begin())>, ElementType>; typename ActualAllocator = std::conditional_t< std::is_same_v<Allocator, void>, std::allocator<ActualElementType>, Allocator>> auto to_vector(Container&& c, ActualAllocator al = ActualAllocator()) { std::vector<ActualElementType, ActualAllocator> v(al); std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
Another solution is to create helper types to do the calculations.
template<typename ElementType, typename Container> using ActualElementType = std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t<decltype(*std::declval<Container>().begin())>, ElementType>; template<typename ActualElementType, typename ActualAllocator = std::conditional_t< std::is_same_v<Allocator, void>, std::allocator<ActualElementType<ElementType, Container>>, Allocator>; template< typename ElementType = void, typename Allocator = void, typename Container> auto to_vector(Container&& c, ActualAllocator<ElementType, Allocator, Container> al = {}) { std::vector<ActualElementType<ElementType, Container>, ActualAllocator<ElementType, Allocator, Container>> v(al); std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
Bonus chatter: There’s still more work to be done with to_vector
, since it doesn’t work with vector<bool>
, thanks to vector<bool>
‘s wacko proxy object, and it doesn’t work with C-style arrays since they don’t have a begin()
method.
Instead, we can use std::iterator_traits
to tell us what the iterator produces. and we can use std::begin
to support C-style arrays.
template<typename ElementType, typename Container> using ActualElementType = std::conditional_t< std::is_same_v<ElementType, void>, std::decay_t< std::iterator_traits< decltype( std::begin(std::declval<Container>())) >::value_type>, ElementType>; template<typename ActualElementType, typename ActualAllocator = std::conditional_t< std::is_same_v<Allocator, void>, std::allocator<ActualElementType<ElementType, Container>>, Allocator>; template< typename ElementType = void, typename Allocator = void, typename Container> auto to_vector(Container&& c, ActualAllocator<ElementType, Allocator, Container> al = {}) { std::vector<ActualElementType<ElementType, Container>, ActualAllocator<ElementType, Allocator, Container>> v(al); std::copy(c.begin(), c.end(), std::back_inserter(v)); return v; }
I guess you wanted to show the template tricks, and the leading default is neat.
But I would probably write this in a different and IMO simpler fashion:
<code>
Nice. I forgot that you can fake default parameters via overloads.
For that last example, did you mean
std::copy(std::begin(c), std::end(c), std::back_inserter(v));
?(I also had to insert
typename
somewhere and change the definition ofActualAllocator
totemplate<typename ElementType, typename Allocator, typename Container> using ActualAllocator =
…)C++23 includes , which is a more generalized version of : It can convert any range into any container.
Example:
<code>
Godbolt Examples
(it also handles the -case)
msvc is currently the only compiler that provides (and only with ).
But in a few years it'll hopefully be available across all compilers, once they fully implement C++23.