Search code examples
c++templatesc++17function-templates

Can type arguments be made deduceable for function templates using std container?


I found this implementation of a few common features of functional programming, e.g. map / reduce: (I'm aware stuff like that is aparently coming or partially present in new C++ versions)

github link

A part of the code:

template <typename T, typename U>
U foldLeft(const std::vector<T>& data,
           const U& initialValue,
           const std::function<U(U,T)>& foldFn) {
    typedef typename std::vector<T>::const_iterator Iterator;
    U accumulator = initialValue;
    Iterator end = data.cend();
    for (Iterator it = data.cbegin(); it != end; ++it) {
        accumulator = foldFn(accumulator, *it);
    }
    return accumulator;
}

template <typename T, typename U>
std::vector<U> map(const std::vector<T>& data, const std::function<U(T)> mapper) {
    std::vector<U> result;
    foldLeft<T, std::vector<U>&>(data, result, [mapper] (std::vector<U>& res, T value)  -> std::vector<U>& {
        res.push_back(mapper(value));
        return res;
    });
    return result;
}

Usage example:

std::vector<int> biggerInts = map<int,int>(test, [] (int num) { return num + 10; });

The type arguments T,U have to be fully qualified for this to compile, as shown in the example, with e.g. map< int,int >( ... ). This implementation is for C++11, as mentioned on the linked-to page.

Is it possible with newer C++ versions (or even 11) now to make the use of this less verbose, i.e. making the types U,T deduce automatically? I have googled for that and only found that there is apparently some improvement for class template, as opposed to function template, argument deduction in C++17. But since I only ever used templates in a rather basic manner, I was wondering whether there is something in existence that I'm not aware of which could improve this implementation verboseness-wise.


Solution

  • More general templates make template argument deduction easier.

    One principle: it is often a mistake to use a std::function as a templated function's parameter. std::function is a type erasure, for use when something needs to store some unknown invokable thing as a specific type. But templates already have the ability to handle any arbitrary invokable type. So if we just use a generic typename FuncT template parameter, it can be deduced for a raw pointer-to-function, a lambda, or another class with operator() directly.

    We might as well also get more general and accept any input container instead of just vector, then determine T from it, if it's even directly needed.

    So for C++11 I would rewrite these:

    // C++20 is adding std::remove_cvref, but it's trivial to implement:
    template <typename T>
    using remove_cvref_t =
        typename std::remove_cv<typename std::remove_reference<T>::type>::type;
    
    template <typename Container, typename U, typename FuncT>
    remove_cvref_t<U> foldLeft(
               const Container& data,
               U&& initialValue,
               const FuncT& foldFn) {
        remove_cvref_t<U> accumulator = std::forward<U>(initialValue);
        for (const auto& elem : data) {
            accumulator = foldFn(std::move(accumulator), elem);
        }
        return accumulator;
    }
    
    template <typename Container, typename FuncT>
    auto map(const Container& data, const FuncT& mapper)
        -> std::vector<remove_cvref_t<decltype(mapper(*std::begin(data)))>>
    {
        using T = remove_cvref_t<decltype(*std::begin(data))>;
        using ResultT = std::vector<remove_cvref_t<decltype(mapper(std::declval<const T&>()))>>;
        ResultT result;
        foldLeft(data, std::ref(result), [&mapper] (ResultT &res, const T& value)  -> ResultT& {
            res.push_back(mapper(value));
            return res;
        });
        return result;
    }
    

    See the working program on coliru.

    There was one unfortunate thing about the old map: it potentially copied the result vector at every iteration. The = in accumulator = foldFn(accumulator, *it); is a self-assignment, which might do nothing, or might allocate new memory, copy contents, then free the old memory and update the container. So instead I've changed the U for foldLeft in this case to a std::reference_wrapper. The = in that case will still "rebind" the wrapper to the same object, but that will at least be quick.

    In C++14 and later, you could do away with finding T within map by using a generic lambda: [&mapper] (std::vector<U>& res, const auto& value) ...