Reordering C++ template type parameters for usability purposes, and type deduction from the future

Raymond Chen

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::decay_t 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;
}

4 comments

Discussion is closed. Login to edit/delete existing comments.

  • Turtlefight 0

    C++23 includes std::ranges::to, which is a more generalized version of to_vector: It can convert any range into any container.
    Example:

    std::list<int> int_list = {1, 2, 3, 4, 5};
    
    // equivalent to to_vector(int_list);
    auto int_vec = std::ranges::to<std::vector>(int_list);
    
    // equivalent to to_vector<long>(int_list);
    auto long_vec = std::ranges::to<std::vector<long>>(int_list);
    
    // equivalent to to_vector<int, my_custom_allocator<int>>(int_list);
    auto vec_with_alloc = std::ranges::to<std::vector>(int_list, my_custom_allocator<int>{});
    

    Godbolt Examples
    (it also handles the std::vector<bool>-case)

    msvc is currently the only compiler that provides std::ranges::to (and only with /std:c++latest).
    But in a few years it’ll hopefully be available across all compilers, once they fully implement C++23.

  • Neil Rashbrook 1

    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 of ActualAllocator to template<typename ElementType, typename Allocator, typename Container> using ActualAllocator = …)

  • Roland Bock 2

    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:

    template <typename ElementType, typename Allocator, typename Container>
    nodiscard constexpr auto to_vector(Container&& c, Allocator al = {}) {
      return std::vector(std::begin(c), std::end(c), al);
    }
    
    template <typename ElementType, typename Container>
    nodiscard constexpr auto to_vector(Container&& c) {
      return to_vector<ElementType, std::allocator>(
          std::forward(c));
    }
    
    template <typename Container>
    nodiscard constexpr auto to_vector(Container&& c) {
      return to_vector<std::decay_t<
          typename std::iterator_traits::value_type>>(
          std::forward(c));
    }
    
    • Raymond ChenMicrosoft employee 0

      Nice. I forgot that you can fake default parameters via overloads.

Feedback usabilla icon