Apply the first valid function of a set of N functions
Asked Answered
H

5

7

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?

Hovel answered 3/2, 2017 at 18:57 Comment(2)
I would suggest instead building a callable object that that has the desired semantics as a more convenient interface: make_first_valid_callable(f1, f2, f3)(a, b, c, d, e);, which does not force users to manually keep track of function count and is more composable.Cleanse
@yurikilochek That's smart. Love it! Now the question is how to solve the problem using that syntax...Hovel
I
5

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.

Instep answered 4/2, 2017 at 4:38 Comment(0)
C
8

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

Cofferdam answered 3/2, 2017 at 19:19 Comment(2)
std::remove_reference_t is redundant.Cleanse
Pass this 1000 lambdas and it will break. ;) (recursive template instantiation limit)Colner
C
5

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)
            )
        );
    }
    // ...
};
Celtic answered 3/2, 2017 at 20:8 Comment(4)
Might want to do std::tuple<std::decay_t<Fs>...> fs_; instead, so that the object can be safely stored.Cleanse
@yurikilochek The only thing I wonder about that is if it requires more of the functions passed in.Celtic
It requires copy/move constructability, naturally. But this is hardly a problem as all std algorithms that use callbacks, std::bind, std::function, std::thread, etc require the same.Cleanse
You probably shouldn't make callable types final, as that will play havoc with naive implementations of utilities like overload with which you may want to compose them (bad example but the only one I can think of offhand; some utilities may wish to derive from the type as a matter of composition, and there's no good reason to disallow it).Caraviello
I
5

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.

Instep answered 4/2, 2017 at 4:38 Comment(0)
C
2

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.

Colner answered 4/2, 2017 at 1:17 Comment(0)
C
1

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

Celtic answered 20/8, 2018 at 21:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.