Search code examples
c++templatesc++11lambdadecltype

Using a type that depends on lambda function as a return type


I want to make a function that takes a lambda as parameter, and returns an object which type depends on the lambda function return type. What I'm trying to achieve is essentially no explicit template parameter at instantiation.

For now, here is my solution, and my question is: is there a shorter (and more elegant) way to do it ?

template<typename Func, typename RT = std::unordered_map<int,
  decltype(((Func*)nullptr)->operator()(T())) > >
RT mapResult(Func func)
{
  RT r;
  for (auto &i : mData)
    r.insert({i.first, func(mData.second)});
  return r;
}

To make it a little more clear, the lambda type Func takes T& as parameter and returns a vector of a certain type, and mapResult maps the result of func in an unordered_map whose _Ty template parameter is the lambda function return type (potentially something else, but still dependent on this type). The actual code is much more complicated, but I'm trying to gain clarity on this point specifically.

The only solution I found to avoid writing the RT type several times was to put it in the template parameters list and give it a default value, dependent on the first template parameter (which itself is deduced from the function argument). It's a little like defining a templated typename.

I'm using VC12, but want to have portable code that compiles under g++ as well.

The instantiation then looks like this (dummy example):

auto r = c.mapResult([](T &t){return std::vector<int> {(int)t.size()};});

Solution

  • The C++11 Standard Library contains a metafunction called result_of. This metafunction computes the return type of a function object. Probably due to its history in boost (and C++03), it is used in a rather peculiar way: You pass it the type of the function object and the type of the arguments you want to call the function object with via a combined function type. For example:

    struct my_function_object
    {
        bool operator()(int);
        char operator()(double);
    };
    
    std::result_of<my_function_object(int)>::type // yields bool
    std::result_of<my_function_object(short)>::type // yields bool
    std::result_of<my_function_object(double)>::type // yields char
    

    result_of performs overload resolution. If you call short s{}; my_function_object{}(s);, overload resolution will select my_function_object::operator()(int). Therefore, the corresponding result_of<my_function_object(short)>::type yields bool.

    Using this trait, you can simplify the computation of the return type as follows:

    template<typename Func, typename RT = std::unordered_map<int,
      typename std::result_of<Func(T&)>::type > >
    RT mapResult(Func func)
    {
      RT r;
      for (auto &i : mData)
        r.insert({i.first, func(i.second)});
      return r;
    }
    

    The T& parameter tells result_of to use an lvalue argument in overload resolution. The default (for a non-reference type T) is xvalue (T&&).

    There is one slight difference to the OP's version: SFINAE will probably not work correctly using std::result_of (in C++11). This was resolved in C++14. See N3462.


    C++14 has introduced standardized alias templates like result_of_t so you can get rid of the typename and ::type:

    template<typename Func, typename RT = std::unordered_map<int,
      std::result_of_t<Func(T&)> > >
    RT mapResult(Func func)
    {
      RT r;
      for (auto &i : mData)
        r.insert({i.first, func(i.second)});
      return r;
    }
    

    If you're using Visual Studio 2013 or newer, you can write alias templates yourself. You could also go one step further and write the whole return type as a metafunction:

    template<typename FT> using result_of_t = typename std::result_of<FT>::type;
    template<typename Func> using RetType =
        std::unordered_map<int, result_of_t<Func(T&)> >;
    
    template<typename Func, typename RT = RetType<Func> >
    RT mapResult(Func func)
    {
      RT r;
      for (auto &i : mData)
        r.insert({i.first, func(i.second)});
      return r;
    }
    

    Of course, if you have sufficient C++14 core language support (not in VS12), you can use return type deduction as well:

    template<typename Func>
    auto mapResult(Func func)
    {
      auto r = std::unordered_map<int, result_of_t<Func(T&)>>{};
      for (auto &i : mData)
        r.insert({i.first, func(i.second)});
      return r;
    }
    

    It is also possible to shorten a version using decltype:

    using std::declval;
    decltype(declval<Func>(T{}))
    

    although this isn't quite correct, both the function object and the argument will be an lvalue:

    decltype(declval<Func&>(declval<T&>{}))
    

    declval will use an xvalue in overload resolution for a non-reference type X. By adding the &, we tell it to use an lvalue instead. (result_of is based on declval, so both show this behaviour.)


    Note that in any case, it might be useful to run the result_of_t<Func(T&)> type through the std::decay metafunction, to get rid e.g. of references which appear in cases such as:

    [](string const& s) -> string const& { return s; } // identity
    

    This does depend on your use case though, and either choice should be documented.


    IIRC, emplace is slightly more efficient (in theory) in this situation (inserting unique elements):

    r.emplace(i.first, func(i.second));
    

    It might be possible to further optimize this function, e.g. by reserving a bucket count before the insertion, or maybe with an iterator adapter to leverage the constructor for insertion. Using std::transform should also be possible, though I'd guess it cannot be as efficient due to additional moves of the value_type pair.