Search code examples
c++c++11templatesfunctional-programmingtemplate-argument-deduction

std::function with templated arguments


I want to write a templated function that applies some function over pairs of elements coming from two vectors. The result should be a new vector of results. I want this to be a templated function so that it works on different types.

I tried the definition before. However, when I try to apply it to some particular function, I get a compilation error.

#include <vector>
#include <cmath>
#include <iostream>
#include <functional>

using namespace std;

template<typename T1, typename T2, typename T3>
vector<T3> mapzip2(const vector<T1> &xs, const vector<T2> &ys, std::function<T3(T1, T2)> l) {
    if (xs.size() != ys.size())
        throw runtime_error("mapzip2: container sizes (" + to_string(xs.size()) + 
                            " and " + to_string(ys.size()) + ") do not match");
    vector<T3> result;
    result.reserve(xs.size());
    for (int i = 0; i < xs.size(); i++)
        result.push_back(l(xs[i], ys[i]));

    return result;
}

constexpr double PRECISION = 1E-6;

bool feq(double a, double b) {
    return abs(a - b) < PRECISION;
}

int main() {
    vector<double> a = {0.3, 0.42, 0.0, -7.34};
    vector<double> b = {0.3, 0.42, 0.0, -7.34};

    // compilation error: no matching function for call to 
    // ‘mapzip2(std::vector<double>&, std::vector<double>&, bool (&)(double, double))’
    vector<bool> result = mapzip2(a, b, feq);

    for (bool b: result) cout << b << ' ';
    cout << endl;
}

What is wrong here with types deduction?


Solution

  • You have a sort of chicken-and-egg problem.

    The T3 type in

    template<typename T1, typename T2, typename T3>
    T3 mapzip2(const vector<T1> &xs, const vector<T2> &ys, std::function<T3(T1, T2)> l)
    

    has to be deduced from the third argument, a std::function<T3(T1, T2)>

    But when you call

    bool feq(double a, double b) {
        return abs(a - b) < PRECISION;
    }
    
    // ...
    
        vector<bool> result = mapzip2(a, b, feq);
    

    you call mapzip() with feq that can be converted to a std::function<bool(double, double)> but isn't a std::function<bool(double, double)>

    So the T3 type can't be deduced as bool because to convert feq to std::function<bool(double, double)> you have to know, before the deduction, that T3 is bool.

    Possible solutions:

    (1) explicit the template types calling mapzip()

    vector<bool> result = mapzip2<double, double, bool>(a, b, feq);
    

    This way the compiler know that T3 is bool, so convert feq to std::function<bool(double, double)>

    (2) construct a std::function<bool(double, double)> with feq

    vector<bool> result = mapzip2(a, b, std::function<double, double, bool>{feq});
    

    so the compiler can receive a std::function as third argument and deduce T3 from it.

    (3) (more flexible and, IMHO, the best of the three) avoid std::function and use a more generic functional typename for the function

    template<typename T1, typename T2, typename F>
    auto mapzip2(const vector<T1> &xs, const vector<T2> &ys, F l) {
        if (xs.size() != ys.size())
            throw runtime_error("mapzip2: container sizes (" + to_string(xs.size()) + 
                                " and " + to_string(ys.size()) + ") do not match");
    
        vector<decltype(l(xs[0], ys[0]))> result; // <-- use decltype() !
    
        result.reserve(xs.size());
        for (int i = 0; i < xs.size(); i++)
            result.push_back(l(xs[i], ys[i]));
    
        return result;
    }
    

    Observe the use of decltype() to deduce the type of the returned vector (the old T3) and the use of auto (starting from C++14) for the returned type of the function.

    If you can't use C++14 (only C++11), you have to add the trailing return type

    template<typename T1, typename T2, typename F>
    auto mapzip2(const vector<T1> &xs, const vector<T2> &ys, F l) 
        -> std::vector<decltype(l(xs[0], ys[0]))>
     {
    
     }
    

    Observe also -- as pointed by ypnos in a comment -- that the signature of your original mapzip2() is wrong: you return result, a std::vector<T3>, not a T3.