0

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.

Community
  • 1
  • 1
sktpin
  • 317
  • 2
  • 15

2 Answers2

3

You can rewrite map signature to be:

template <typename T, typename M, typename U = decltype(std::declval<M>()(T{}))>
std::vector<U> map(const std::vector<T>& data, const M mapper)

then T will be deduced as value_type of vector's items.

M is any callable object.

U is deduced as return type of M() functor when called for T{}.

Below

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

works fine.

Live demo

rafix07
  • 20,001
  • 3
  • 20
  • 33
  • Should also be noted that this already works in C++11, so the author just chose to not implement it that way for some reason. – walnut Apr 01 '20 at 11:46
  • I'd change `T{}` to `std::declval()`, both in case `T` isn't default-constructible, and in case `M` is strangely overloaded to give different result types for `T&&` and `const T&`. – aschepler Apr 01 '20 at 12:43
  • 1
    This did the trick and I upvoted it. After some hesitation and hovering over the accept checkmark it read "... or the most helpful". Since the other reply explained/mentioned some things that weren't on my radar yet with regards to understanding all of this, I chose to "accept" that one. – sktpin Apr 03 '20 at 08:10
1

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) ...

aschepler
  • 70,891
  • 9
  • 107
  • 161