Search code examples
c++templatesoverloadingtemplate-specializationoverload-resolution

C++ function template specialization based on the templated return type


In C++20, instead of doing

size_t count = 42;
std::vector<int> my_vector;
vec.reserve(count);
std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));

I want to be able to just say

std::vector<int> my_vector = get_from_cin<int>(42);

Problem is, I also want to have a similar facility for std::list:

std::list<int> my_list = get_from_cin<int>(42);

so I tried defining two function templates:

template<typename T>
std::vector<T> get_from_cin(size_t count) {
    std::vector<T> vec;
    vec.reserve(count);
    std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(vec));
    return vec;
}

template<typename T>
std::list<T> get_from_cin(size_t count) {
    std::list<T> list;
    std::copy_n(std::istream_iterator<T>(std::cin), count, std::back_inserter(list));
    return list;
}

int main() {
    std::vector<int> my_vector = get_from_cin<int>(42);
}

which (expectedly) lead to ambiguity when specializing the template:

main.cpp:36:34: error: call to 'get_from_cin' is ambiguous
   36 |     std::vector<int> my_vector = get_from_cin<int>(42);
      |                                  ^~~~~~~~~~~~~~~~~
...

Godbolt link: https://godbolt.org/z/EbdxTEcns

How can I modify the code so that this ambiguity is eliminated and the resulting code for calling the templated function is as concise as possible?

PS. I would just name the two functions differently but that feels contrary to the C++ style.


Solution

  • If you can use C++23, then something like this can help:

    #include <ranges>
    #include <cassert>
    auto vec = std::views::istream<int>(cin)
             | std::views::take(42)
             | std::ranges::to<std::vector>();
    assert((size(vec) == 42));
    

    The downside is however, that if user inputs 43 numbers instead, the 43th number will be dropped for ever. Special case of counted input iterator is an unsolved problem within the realms of std. The reliable solution is still reserve followed by copy_n. I don't approve of template<template> syntax in user code, so I don't follow that route. But something like this can be useful:

    template<std::ranges::range C>
        requires (not std::ranges::view<C>)
    auto from_istream(std::istream& ins, std::size_t n, auto&& ...args)
        requires std::constructible_from<C, decltype(args)...>
    {
        C res{std::forward<decltype(args)>(args)...};
    
        if constexpr (
           requires  (C cnt, std::size_t sz)
           { cnt.reserve(sz); }
        )    res.reserve(n + res.size());
    
        std::copy_n
           ( std::istream_iterator
                < std::ranges::range_value_t<C> >(ins)
           , n, std::back_inserter(res));
    
        return res;//nrvo copy elision.   
    };
    

    Now you can instantiate it like this:

    auto vec = from_istream<std::vector<int>>(cin,42);
    assert((vec.size == 42));
    auto lst = from_istream<std::list<int>>(cin,42);
    

    In either case you need the type id of yor object on the right hand side of the assignment to instruct compiler what to create. Then type deduction and direct initialization handle the rest.