How to deduce the return type of a function object from parameters list?

℡╲_俬逩灬. 提交于 2019-12-31 10:32:40

问题


I'm trying to write a projection function that could transform a vector<T> into a vector<R>. Here is an example:

auto v = std::vector<int> {1, 2, 3, 4};
auto r1 = select(v, [](int e){return e*e; }); // {1, 4, 9, 16}
auto r2 = select(v, [](int e){return std::to_string(e); }); // {"1", "2", "3", "4"}

First attempt:

template<typename T, typename R>
std::vector<R> select(std::vector<T> const & c, std::function<R(T)> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

But for

auto r1 = select(v, [](int e){return e*e; });

I get:

error C2660: 'select' : function does not take 2 arguments

I have to explicitly call select<int,int> to work. I don't like this because the types are redundant.

auto r1 = select<int, int>(v, [](int e){return e*e; }); // OK

Second attempt:

template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

The result is same error, function does not take 2 arguments. In this case I actually have to supply a 3rd type argument:

auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });

Third attempt:

template<typename T, typename R, template<typename, typename> class Selector>
std::vector<R> select(std::vector<T> const & c, Selector<T,R> s)
{
   std::vector<R> v;
   std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
   return v;
}

For

auto r1 = select<int, int, std::function<int(int)>>(v, [](int e){return e*e; });

the error is:

'select' : invalid template argument for 'Selector', class template expected

For

auto r1 = select(v, [](int e){return e*e; });

error C2660: 'select' : function does not take 2 arguments

(I know the last two attempts are not particularly great.)

How can I write this select() template function to work for the sample code I put in the beginning?


回答1:


Option #1:

Basic decltype() usage:

template <typename T, typename F>
auto select(const std::vector<T>& c, F f)
    -> std::vector<decltype(f(c[0]))>
{
    using R = decltype(f(c[0]));
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}

Option #2:

Basic std::result_of<T> usage:

template <typename T, typename F, typename R = typename std::result_of<F&(T)>::type>
std::vector<R> select(const std::vector<T>& c, F f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), f);
    return v;
}

Option #3:

Advanced decltype() usage and perfect-forwarding (see notes*):

template <typename T, typename A, typename F>
auto select(const std::vector<T, A>& c, F&& f)
    -> std::vector<typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type>
{
    using R = typename std::decay<decltype(std::declval<typename std::decay<F>::type&>()(*c.begin()))>::type;
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}

Option #4:

Advanced std::result_of<T> usage and perfect-forwarding (see notes*):

template <typename T, typename A, typename F, typename R = typename std::decay<typename std::result_of<typename std::decay<F>::type&(typename std::vector<T, A>::const_reference)>::type>::type>
std::vector<R> select(const std::vector<T, A>& c, F&& f)
{
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c)
                 , std::back_inserter(v)
                 , std::forward<F>(f));
    return v;
}

* Note: Options #3 and #4 assume that the std::transform algorithm takes a function object by-value, and then uses it as a non-const lvalue. This is why one can see this strange typename std::decay<F>::type& syntax. If the function object is supposed to be called within the select function itself, and the result type is not going to be used as a container's template argument (for the purpose of what the outer-most std::decay<T> is used), then the correct and portable syntax for obtaining the return type is:

/*#3*/ using R = decltype(std::forward<F>(f)(*c.begin()));

/*#4*/ typename R = typename std::result_of<F&&(typename std::vector<T, A>::const_reference)>::type



回答2:


Your first problem is that you think that a lambda is a std::function. A std::function and a lambda are unrelated types. std::function<R(A...)> is a type erasure object that can convert anything that is (A) copyable, (B) destroyable and (C) can be invoked using A... and returns a type compatible with R, and erases all other information about the type.

This means it can consume completely unrelated types, so long as they pass those tests.

A lambda is an anonymous class which is destroyable, can be copied (except in C++14, where this is sometimes), and has an operator() which you specify. This means you can often convert a lambda into a std::function with a compatible signature.

Deducing the std::function from the lambda is not a good idea (there are ways to do it, but they are bad ideas: C++14 auto lambdas break them, plus you get needless inefficiency.)

So how do we solve your problem? As I see it, your problem is taking a function object and a container and deducing what kind of element transform would produce after applying the function object on each element, so you can store the result in a std::vector.

This is the answer that is closest to the solution to your problem:

template<typename T, typename R, typename Selector>
std::vector<R> select(std::vector<T> const & c, Selector s) {
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}

The easiest thing to do is swap T and R in template order, and have the caller pass in R explicitly, like select<double>. This leaves T and Selector being deduced. This isn't ideal, but it does do a small improvement.

For a full solution, there are two ways to approach fixing this solution. First, we can change select to return a temporary object with an operator std::vector<R>, delaying the transformation to that point. Here is an incomplete sketch:

template<typename T, typename Selector>
struct select_result {
  std::vector<T> const& c;
  Selector s;
  select_result(select_result&&)=default;
  select_result(std::vector<T> const & c_, Selector&& s_):
    c(c_), s(std::forward<Selector>(s_)
  {}
  operator std::vector<R>()&& {
    std::vector<R> v;
    std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
    return v;
  }
};
template<typename T, typename Selector>
select_result<T, Selector> select(std::vector<T> const & c, Selector&& s) {
  return {c, std::forward<Selector>(s)};
}

I can also provide a slicker version that sadly relies on undefined behavior (reference capture of local references in a function have lifetime issues under the standard).

But that gets rid of auto v = select syntax -- you end up storing the thing that produces results, rather than the results.

You can still do std::vector<double> r = select( in_vec, [](int x){return x*1.5;} ); and it works pretty well.

Basically I have split deduction into two phases, one for arguments, and one for return value.

However, there is little need to rely on that solution, as there are other more direct ways.

For a second approach, we can deduce R ourselves:

template<typename T, typename Selector>
std::vector<typename std::result_of<Selector(T)>::type>
select(std::vector<T> const & c, Selector s) {
  using R = typename std::result_of<Selector(T)>::type;
  std::vector<R> v;
  std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
  return v;
}

which is a pretty solid solution. A touch of cleanup:

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 
template<
  typename T, typename A,
  typename Selector,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(std::vector<T, A> const & c, Selector&& s) {
  std::vector<R> v;
  std::transform(begin(c), end(c), back_inserter(v), std::forward<Selector>(s));
  return v;
}

makes this a serviceable solution. (moved R to template type lists, allowed alternative allocators to the vector, removed some needless std::, and did perfect forwarding on the Selector).

However, we can do better.

The fact that the input is a vector is pretty pointless:

template<
  typename Range,
  typename Selector,
  typename R=typename std::result_of<Selector(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

which doesn't compile due to the inability to determine T as yet. So lets work on that:

namespace details {
  namespace adl_aux {
    // a namespace where we can do argument dependent lookup on begin and end
    using std::begin; using std::end;
    // no implementation, just used to help with ADL based decltypes:
    template<class R>
    decltype( begin( std::declval<R>() ) ) adl_begin(R&&);
    template<class R>
    decltype( end( std::declval<R>() ) ) adl_end(R&&);
  }
  // pull them into the details namespace:
  using adl_aux::adl_begin;
  using adl_aux::adl_end;
}
// two aliases.  The first takes a Range or Container, and gives
// you the iterator type:
template<class Range>
using iterator = decltype( details::adl_begin( std::declval<Range&>() ) );
// the second is syntactic sugar on top of `std::iterator_traits`:
template<class Iterator>
using value_type = typename std::iterator_traits<Iterator>::value_type;

which gives us an iterator<Range> and value_type<Iterator> aliases. Together they let us deduce T easily:

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<iterator<Range&>>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

and bob is your uncle. (decayed_lvalue reflects how the Selector type is used for corner cases, and iterator<Range&> reflects that we are getting an iterator from the lvalue version of Range).

In VS2013 sometimes the above decltypes confuse the half-implementation of C++11 they have. Replacing iterator<Range> with decltype(details::adl_begin(std::declval<Range>())) as ugly as that is can fix that issue.

// std::transform takes by-value, then uses an lvalue:
template<class T>
using decayed_lvalue = typename std::decay<T>::type&; 

template<
  typename Range,
  typename Selector,
  typename T=value_type<decltype(details::adl_begin(std::declval<Range&>()))>,
  typename R=typename std::result_of<decayed_lvalue<Selector>(T)>::type
>
std::vector<R> select(Range&& in, Selector&& s) {
  std::vector<R> v;
  using std::begin; using std::end;
  std::transform(begin(in), end(in), back_inserter(v), std::forward<Selector>(s));
  return v;
}

The resulting function will take arrays, vectors, lists, maps, or custom written containers, and will take any transformation function, and produce a vector of the resulting type.

The next step is to make the transformation lazy instead of putting it directly into a vector. You can have as_vector which takes a range and writes it out to a vector if you need to get rid of lazy evaluation. But that is getting into writing an entire library rather than solving your problem.



来源:https://stackoverflow.com/questions/26383517/how-to-deduce-the-return-type-of-a-function-object-from-parameters-list

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!