Apply the first valid function of a set of N functions

早过忘川 提交于 2019-11-28 10:14:15

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.

In :

#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 :

#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

Justin

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)
            )
        );
    }
    // ...
};

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.

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

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