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

后端 未结 2 1060
说谎
说谎 2021-02-02 10:13

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

auto v =          


        
2条回答
  •  孤独总比滥情好
    2021-02-02 11:00

    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 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
    std::vector select(std::vector const & c, Selector s) {
      std::vector 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. 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, delaying the transformation to that point. Here is an incomplete sketch:

    template
    struct select_result {
      std::vector const& c;
      Selector s;
      select_result(select_result&&)=default;
      select_result(std::vector const & c_, Selector&& s_):
        c(c_), s(std::forward(s_)
      {}
      operator std::vector()&& {
        std::vector v;
        std::transform(std::begin(c), std::end(c), std::back_inserter(v), s);
        return v;
      }
    };
    template
    select_result select(std::vector const & c, Selector&& s) {
      return {c, std::forward(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 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
    std::vector::type>
    select(std::vector const & c, Selector s) {
      using R = typename std::result_of::type;
      std::vector 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
    using decayed_lvalue = typename std::decay::type&; 
    template<
      typename T, typename A,
      typename Selector,
      typename R=typename std::result_of(T)>::type
    >
    std::vector select(std::vector const & c, Selector&& s) {
      std::vector v;
      std::transform(begin(c), end(c), back_inserter(v), std::forward(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::type
    >
    std::vector select(Range&& in, Selector&& s) {
      std::vector v;
      using std::begin; using std::end;
      std::transform(begin(in), end(in), back_inserter(v), std::forward(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
        decltype( begin( std::declval() ) ) adl_begin(R&&);
        template
        decltype( end( std::declval() ) ) 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
    using iterator = decltype( details::adl_begin( std::declval() ) );
    // the second is syntactic sugar on top of `std::iterator_traits`:
    template
    using value_type = typename std::iterator_traits::value_type;
    

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

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

    and bob is your uncle. (decayed_lvalue reflects how the Selector type is used for corner cases, and iterator 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 with decltype(details::adl_begin(std::declval())) as ugly as that is can fix that issue.

    // std::transform takes by-value, then uses an lvalue:
    template
    using decayed_lvalue = typename std::decay::type&; 
    
    template<
      typename Range,
      typename Selector,
      typename T=value_type()))>,
      typename R=typename std::result_of(T)>::type
    >
    std::vector select(Range&& in, Selector&& s) {
      std::vector v;
      using std::begin; using std::end;
      std::transform(begin(in), end(in), back_inserter(v), std::forward(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.

提交回复
热议问题