Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Apply the first valid function of a set of N functions

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?

like image 372
Vincent Avatar asked Feb 03 '17 18:02

Vincent


4 Answers

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

like image 186
Piotr Skotnicki Avatar answered Nov 04 '22 20:11

Piotr Skotnicki


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.

like image 36
Barry Avatar answered Nov 04 '22 21:11

Barry


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

like image 33
Justin Avatar answered Nov 04 '22 21:11

Justin


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.

like image 21
Yakk - Adam Nevraumont Avatar answered Nov 04 '22 19:11

Yakk - Adam Nevraumont