Apply the first valid function of a set of N functions

吃可爱长大的小学妹 提交于 2019-11-27 03:30:24

问题


This previous answer shows how to apply function based on the validity of a call: here. However, it applies to two functions. I was wondering if the concept could be generalized to N functions using smart template programming tricks, in C++14.

The problem is the following:

template <std::size_t N, class... X>
/* [Return type] */ apply_on_validity(X&&... x)
{
    // Tricks here
}

// The function would be equivalent to a hypothetical
// template <std::size_t N, class... F, class... Args>
// [Return type] apply_on_validity(F&&... f, Args&&... args)
// where N = sizeof...(F) is the number of functions

On the execution side:

apply_on_validity<3>(f1, f2, f3, a, b, c, d, e);

Would:

  • call f1(a, b, c, d, e) if the expression is valid, otherwise
  • call f2(a, b, c, d, e) if the expression is valid, otherwise
  • call f3(a, b, c, d, e) if the expression is valid, otherwise
  • do nothing

And in all cases, it would return the result of the function that was executed.

On the calling side, the template parameter 3 indicates the number of functions that have been specified.

Is it doable in C++14, and if so, how?


回答1:


Here's another fun one for kicks, abusing overload resolution. We're going to transform each function into another function that takes a rank argument, combine all of our functions together, and then just invoke them. The selection will just fall out naturally from the overload set.

Our ranker is just this ladder of types:

template <int I> struct rank : rank<I-1> { };
template <> struct rank<0> { };

We need a function transformer:

template <class T, class F>
auto prepend_arg(F&& f) {
    return [f=std::forward<F>(f)](T, auto&&... args)
        -> decltype(std::forward<F>(f)(std::forward<decltype(args)>(args)...))
    {
        return std::forward<F>(f)(std::forward<decltype(args)>(args)...);
    };
}

And then:

template <std::size_t N, std::size_t... Is, std::size_t... Js,
    class... X>
decltype(auto) apply_impl(std::index_sequence<Is...>,
                          std::index_sequence<Js...>,
                          X&&... x)
{
    auto all = std::forward_as_tuple(std::forward<X>(x)...);

    return overload(
        // all of our function cases
        prepend_arg<rank<N-Is>>(std::get<Is>(all))...,
        // base case: do nothing
        [](rank<0>, auto&&...) {}
    )(rank<N>{}, std::get<N+Js>(all)...);
//               ^^^^^^^^^^^^^^^^^^^^^^^^
//               pass in all the arguments    
}

template <std::size_t N, class... X>
decltype(auto) apply_on_validity(X&&... x) {
    return apply_impl<N>(
        std::make_index_sequence<N>{},
        std::make_index_sequence<sizeof...(X)-N>{},
        std::forward<X>(x)...);
}

I leave overload() as an exercise.




回答2:


In c++14:

#include <tuple>
#include <utility>
#include <type_traits>
#include <cstddef>

template <std::size_t N, std::size_t... Js, typename Args>
auto apply_on_validity_impl(int, std::integral_constant<std::size_t, N>, std::index_sequence<Js...>, Args&& args)
{
    // Nothing here
}

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
auto apply_on_validity_impl(int, std::integral_constant<std::size_t, I>, std::index_sequence<Js...>, Args&& args)
    -> decltype(std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...))
{
    return std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...);
}

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
decltype(auto) apply_on_validity_impl(char, std::integral_constant<std::size_t, I>, std::index_sequence<Js...> seq, Args&& args)
{
    return apply_on_validity_impl<N>(0, std::integral_constant<std::size_t, I + 1>{}, seq, std::forward<Args>(args));
}

template <std::size_t N, typename... Args>
decltype(auto) apply_on_validity(Args&&... args)
{
    return apply_on_validity_impl<N>(0, std::integral_constant<std::size_t, 0>{}, std::make_index_sequence<sizeof...(Args) - N>{}, std::forward_as_tuple(std::forward<Args>(args)...));
}

DEMO

In c++17:

#include <tuple>
#include <utility>
#include <type_traits>
#include <cstddef>

template <std::size_t N, std::size_t I, std::size_t... Js, typename Args>
decltype(auto) apply_on_validity_impl(std::index_sequence<Js...> seq, Args&& args)
{        
    if constexpr (I == N)
    {
    }
    else if constexpr (std::is_invocable_v<std::tuple_element_t<I, Args>, std::tuple_element_t<Js + N, Args>...>)
    {
        return std::get<I>(std::forward<Args>(args))(std::get<Js + N>(std::forward<Args>(args))...);
    }
    else
    {
        return apply_on_validity_impl<N, I + 1>(seq, std::forward<Args>(args));
    }
}

template <std::size_t N, typename... Args>
decltype(auto) apply_on_validity(Args&&... args)
{
    return apply_on_validity_impl<N, 0>(std::make_index_sequence<sizeof...(Args) - N>{}, std::forward_as_tuple(std::forward<Args>(args)...));
}

DEMO 2




回答3:


The syntax you are using is kind of awkward, since you have to know exactly how many functions you have. Combined with Piotr Skotnicki's solution, this solves that problem:

// Would be a local class except for the fact that it needs
// to have templates, thus can't be local to the function.
template<class... Fs>
class apply_first_f final {
public:
    apply_first_f(Fs&&... fs)
        : fs_{ std::forward<Fs>(fs)... }
    {}

    template<class... Args>
    decltype(auto) operator()(Args&&... args) const
    {
        return apply_impl<sizeof...(Args)>(std::make_index_sequence<sizeof...(Fs)>{}, std::forward_as_tuple(std::forward<Args>(args)...));
    }
private:
    std::tuple<std::decay_t<Fs>...> fs_;

    template<size_t argsSize, size_t... Is, class Args>
    decltype(auto) apply_impl(std::index_sequence<Is...>, Args&& args) const
    {
        return apply_on_validity_impl<sizeof...(Fs)>(
            0,
            std::integral_constant<size_t, 0>{},
            std::make_index_sequence<argsSize>{},
            std::tuple_cat(
                std::forward_as_tuple(std::get<Is>(fs_)...),
                std::forward<Args>(args)
            )
        );
    }
};

template<class... Fs>
auto make_apply_first_valid_f(Fs&&... fs)
{
    return apply_first_f<Fs...>{ std::forward<Fs>(fs)... };
}

And the function can be used like so:

make_apply_first_valid_f(f1, f2, f3)(args);

DEMO (adapted from Piotr's demo)

Using it with Piotr's C++ 1z example requires just a small change:

template<class... Fs>
class apply_first_f final {
    // ...
    template<size_t argsSize, size_t... Is, class Args>
    decltype(auto) apply_impl(std::index_sequence<Is...>, Args&& args) const
    {
        return apply_on_validity_impl<sizeof...(Fs), /* added */ 0>(
            /* 0, */
            /* std::integral_constant<size_t, 0>{}, */
            std::make_index_sequence<argsSize>{},
            std::tuple_cat(
                std::forward_as_tuple(std::get<Is>(fs_)...),
                std::forward<Args>(args)
            )
        );
    }
    // ...
};



回答4:


I had answered this in the previous iteration, but the nary version had a bug, and clang had an internal compiler error which made it hard to find the bug.

I have since fixed the bug. So here is a pile of metaprogramming, followed by solving your problem.

First, a homebrew version of C++2a's is_detected:

#include <utility>
#include <type_traits>
#include <iostream>
#include <tuple>

namespace details {
  template<class...>using void_t=void;
  template<template<class...>class Z, class=void, class...Ts>
  struct can_apply:std::false_type{};
  template<template<class...>class Z, class...Ts>
  struct can_apply<Z, void_t<Z<Ts...>>, Ts...>:std::true_type{};
}
template<template<class...>class Z, class...Ts>
using can_apply = typename details::can_apply<Z, void, Ts...>::type;

As it happens, std::result_of_t is the trait we want to test.

template<class Sig>
using can_call = can_apply< std::result_of_t, Sig >;

now can_call< Some(Sig,Goes,Here) > is true_type iff the expression you want can be called.

Now we write some compile-time if dispatch machinery.

template<std::size_t I>
using index_t=std::integral_constant<std::size_t, I>;
template<std::size_t I>
constexpr index_t<I> index_v{};

constexpr inline index_t<0> dispatch_index() { return {}; }
template<class B0, class...Bs,
  std::enable_if_t<B0::value, int> =0
>
constexpr index_t<0> dispatch_index( B0, Bs... ) { return {}; }
template<class B0, class...Bs,
  std::enable_if_t<!B0::value, int> =0
>
constexpr auto dispatch_index( B0, Bs... ) { 
  return index_v< 1 + dispatch_index( Bs{}...) >;
}

template<class...Bs>
auto dispatch( Bs... ) {
  using I = decltype(dispatch_index( Bs{}... ));
  return [](auto&&...args){
    return std::get<I::value>( std::make_tuple(decltype(args)(args)..., [](auto&&...){}) );
  };
}

dispatch( SomeBools... ) returns a lambda. The first of the SomeBools which is compile-time truthy (has a ::value that evaluates to true in a boolean context) determines what the returned lambda does. Call that the dispatch index.

It returns the dispatch_index'd argument to the next call, and an empty lambda if that is one-past-the-end of the list.

Now we want to be able to dispatch on a set of possibilities. To make this simple (heh), we'll use index_over:

template<class=void,  std::size_t...Is >
auto index_over( std::index_sequence<Is...> ){
  return [](auto&&f)->decltype(auto){
    return decltype(f)(f)( std::integral_constant<std::size_t, Is>{}... );
  };
}
template<std::size_t N>
auto index_over(std::integral_constant<std::size_t, N> ={}){
  return index_over(std::make_index_sequence<N>{} );
}

which lets us expand parameter packs without having to build new functions.

Then we can write auto_dispatch as a single function template:

template<class...Fs>
auto auto_dispatch( Fs&&... fs ) {
  auto indexer =  index_over<sizeof...(fs)>();
  // some compilers dislike lambdas with unexpanded parameter packs.
  // this helps with that:
  auto helper = [&](auto I)->decltype(auto){ 
    return std::get<decltype(I)::value>( std::forward_as_tuple( decltype(fs)(fs)... ) );
  };
  // Get 0 through N-1 as compile-time constants:
  return indexer
  (
    [helper](auto...Is){
      // make tuple of functions:
      auto fs_tuple = std::forward_as_tuple( helper(Is)... );
      // This is what is returned from the `auto_dispatch` function
      // it perfect forwards into the correct lambda passed to `auto_dispatch`
      // based on which is the first one which can be invoked by
      // args...
      return [fs_tuple](auto&&...args) {
        // dispatcher knows which one can be called
        auto dispatcher = dispatch(can_call<Fs(decltype(args)...)>{}...);
        // here we get the first one that can be called, or an empty lambda:
        auto&& f0 = dispatcher(std::get<decltype(Is)::value>(fs_tuple)...);
        // here we do the actual call:
        std::forward<decltype(f0)>(f0)(decltype(args)(args)...);
      };
    }
  );
}

with test code:

auto a = [](int x){ std::cout << x << "\n"; };
auto b = [](std::string y){ std::cout << y << "\n";  };
struct Foo {};
auto c = [](Foo){ std::cout << "Foo\n";  };
int main() {
  auto_dispatch(a, c)( 7 );
  auto_dispatch(a, c)( Foo{} );
  auto_dispatch(a, b, c)( Foo{} );
  auto_dispatch(a, b, c)( "hello world" );
}

Live example

The only N-ary recursive template instantiation above is dispatch_index. I can make that log-depth with a bit of work (divide and conquer). Getting it constant depth is hard. I will think on it.




回答5:


Use boost::hana::overload_linearly:

hana::overload_linearly(f1, f2, f3)(a, b, c, d, e)

If none of the expressions are valid, it's a compile error, but it's easy to make it do nothing in that case:

hana::overload_linearly(f1, f2, f3, [](auto&&...) {})(a, b, c, d, e)

Alternatively, use boost::hof::first_of, which does the same thing



来源:https://stackoverflow.com/questions/42031216/apply-the-first-valid-function-of-a-set-of-n-functions

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