Search code examples
c++templateslambdatemplate-argument-deductiongeneric-lambda

How to deduce the return type of a lambda?


I want to mimic Ruby's map() method in C++. I am struggling to figure out the return type automatically:

#include <vector>
#include <string>
#include <algorithm>
#include <iostream>

typedef std::string T2;

template<class T1,
//  class T2, // gives "couldn't deduce template parameter 'T2'"
    class UnaryPredicate>
std::vector<T2> map(std::vector<T1> in, UnaryPredicate pred)
{
    std::vector<T2> res(in.size());
    std::transform(in.begin(), in.end(), res.begin(), pred);
    return res;
}

int main()
{
    std::vector<int> v1({1,2,3});
    auto v2(map(v1, [](auto el) { return "'"+std::to_string(el+1)+"'"; }));
    std::cout << v2[0] << "," << v2[1] << "," << v2[2] << std::endl;
}

This way it compiles, but T2 is fixed to string. If I use the other T2 definition the compiler complains couldn't deduce template parameter 'T2'. I also tried to make use of std::declval, but probably not the right way - I was unable to solve the problem.


Solution

  • Use decltype + std::decay_t:

    template <class T, class UnaryPredicate>
    auto map(const std::vector<T>& in, UnaryPredicate pred)
    {
        using result_t = std::decay_t<decltype(pred(in[0]))>;
    
        std::vector<result_t> res;
        res.reserve(in.size());
        std::transform(in.begin(), in.end(), std::back_inserter(res), pred);
        return res;
    }
    

    Example usage:

    std::vector v1{1, 2, 3};
    auto v2 = map(v1, [](int el) { return "'" + std::to_string(el + 1) + "'"; });
    std::cout << v2[0] << ", " << v2[1] << ", " << v2[2] << '\n';
    

    (live demo)

    Please also note the following changes that I made:

    1. I changed in to take by const reference instead of by value. This avoids unnecessary copies.

    2. I used reserve + back_inserter, instead of value initialization + assignment.

    3. I used auto as the return type. This enables return type deduction. The res vector is guaranteed not to be copied. It is also eligible for copy elision.

    4. You can list-initialize from a braced-init-list directly, so remove the parentheses surrounding the braced-init-list.

    5. std::endl should not be used when \n is sufficient. std::endl causes the buffer to be flushed, while \n does not. Unnecessary flushing can cause performance degradation. See std::endl vs \n.