Choose function to apply based on the validity of an expression
Asked Answered
B

4

17

The problem is the following, in C++14:

  • Let's have two functions FV&& valid_f, FI&& invalid_f, and arguments Args&&... args
  • The function apply_on_validity should apply valid_f on args if the expression std::forward<FV>(valid_f)(std::forward<Args>(args)...) is valid
  • Otherwise and if std::forward<FV>(invalid_f)(std::forward<Args>(args)...) is a valid expression, apply_on_validity should apply invalid_f on args
  • Otherwise apply_on_validity should do nothing

I guess the code should look like something like this:

template <class FV, class FI, class... Args, /* Some template metaprog here */>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    // Apply valid_f by default
    std::forward<FV>(valid_f)(std::forward<Args>(args)...);
}

template <class FV, class FI, class... Args, /* Some template metaprog here */>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    // Apply invalid_f if valid_f does not work
    std::forward<FV>(invalid_f)(std::forward<Args>(args)...);
}

template <class FV, class FI, class... Args, /* Some template metaprog here */>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    // Do nothing when neither valid_f nor invalid_f work
}

But I don't really know how to do that. Any idea?


Link to the generalization here.

Brevier answered 3/2, 2017 at 17:44 Comment(5)
You may consider looking at void_t and having SFINAE based in a void_t expression.Cochise
but do you want this when valid_f and invalid_f are arguments of apply_on_validity()? Not with two know and fixed functions?Wu
Cannot wait for if constexpr...Underthrust
@Underthrust Same here :)Brevier
@Underthrust what do you mean, wait? It's already in clang 3.9 from apt.llvm.org and in gcc SVN tip of trunk (Ubuntu 17.04 testing distro has .deb packages)Marshamarshal
K
17

Take:

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

and then:

template <class FV, class FI, class... Args>
auto apply_on_validity_impl(rank<2>, FV&& valid_f, FI&& invalid_f, Args&&... args)
    -> decltype(std::forward<FV>(valid_f)(std::forward<Args>(args)...), void())
{
    std::forward<FV>(valid_f)(std::forward<Args>(args)...);
}

template <class FV, class FI, class... Args>
auto apply_on_validity_impl(rank<1>, FV&& valid_f, FI&& invalid_f, Args&&... args)
    -> decltype(std::forward<FI>(invalid_f)(std::forward<Args>(args)...), void())
{
    std::forward<FI>(invalid_f)(std::forward<Args>(args)...);
}

template <class FV, class FI, class... Args>
void apply_on_validity_impl(rank<0>, FV&& valid_f, FI&& invalid_f, Args&&... args)
{

}

template <class FV, class FI, class... Args>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    return apply_on_validity_impl(rank<2>{}, std::forward<FV>(valid_f), std::forward<FI>(invalid_f), std::forward<Args>(args)...);
}

DEMO

Kissable answered 3/2, 2017 at 17:55 Comment(2)
a little explanation: the decltype uses SFINAE to ensure that the template function is enabled for the specified expression. The rank parameter is there to disambiguate and prioritize the overloads in case that more than one template function overload is valid.Claiborne
Well, I just finished my solution, and discovered you had posted exactly what I came up with.Delude
M
11

Piotr Skotnicki's answer is superb, but code like that makes me feel compelled to point out how much cleaner C++17 will be thanks to constexpr if and additional type traits like is_callable: Demo Demo*This version creates more warnings but is simpler

template <class FV, class FI, class... Args>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    if constexpr (std::is_callable_v<FV(Args...)>)
        std::cout << "Apply valid_f by default\n";
    else
    {
        if constexpr (std::is_callable_v<FI(Args...)>)
            std::cout << "Apply invalid_f if valid_f does not work\n";
        else
            std::cout << "Do nothing when neither valid_f nor invalid_f work\n";
    }
}
Millionaire answered 3/2, 2017 at 18:35 Comment(2)
You could replace those complicated is_callable_v checks with something like is_callable_v<FV(Args...)>, for most scenarios. When is this not the case? I believe these are exactly equivalent in this context (i.e. when used on types deduced from forwarding references).Case
@Underthrust and yuri kilochek: I was initially thinking of ref-qualified functors when I wrote that, but it seems that is_callable has it covered. Now gcc likes to give me lots of warnings, but it worksMillionaire
U
7

Here's an alternative answer, just for kicks. We need a static_if:

template <class T, class F> T&& static_if(std::true_type, T&& t, F&& ) { return std::forward<T>(t); }
template <class T, class F> F&& static_if(std::false_type, T&& , F&& f) { return std::forward<F>(f); }

And an is_callable. Since you're just supporting functions, we can do it as:

template <class Sig, class = void>
struct is_callable : std::false_type { };

template <class F, class... Args>
struct is_callable<F(Args...), void_t<decltype(std::declval<F>()(std::declval<Args>()...))>>
: std::true_type
{ };

And then we can construct the logic in place:

template <class FV, class FI, class... Args>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
    auto noop = [](auto&&...) {};

    static_if(
        is_callable<FV&&(Args&&...)>{},
        std::forward<FV>(valid_f),
        static_if(
            std::is_callable<FI&&(Args&&...)>{},
            std::forward<FI>(invalid_f),
            noop
        )
    )(std::forward<Args>(args)...);
}
Underthrust answered 3/2, 2017 at 19:0 Comment(1)
@Is this method generalizable here: #42031716?Brevier
P
3

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.

template <class FV, class FI, class... Args /*, Some template metaprog here */>
void apply_on_validity(FV&& valid_f, FI&& invalid_f, Args&&... args)
{
  dispatch(
    can_call<FV(Args...)>{},
    can_call<FI(Args...)>{}
  )(
    [&](auto&& valid_f, auto&&)->decltype(auto) {
      return decltype(valid_f)(valid_f)(std::forward<Args>(args)...);
    },
    [&](auto&&, auto&& invalid_f)->decltype(auto) {
      return decltype(invalid_f)(valid_f)(std::forward<Args>(args)...);
    }
  )(
    valid_f, invalid_f
  );
}

and done, live example.

We could make this generic to enable nary version. First 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>{} );
}

Then auto_dispatch:

template<class...Fs>
auto auto_dispatch( Fs&&... fs ) {
  auto indexer =  index_over<sizeof...(fs)>();
  auto helper = [&](auto I)->decltype(auto){ 
    return std::get<decltype(I)::value>( std::forward_as_tuple( decltype(fs)(fs)... ) );
  };
  return indexer
  (
    [helper](auto...Is){
      auto fs_tuple = std::forward_as_tuple( helper(Is)... );
      return [fs_tuple](auto&&...args) {
        auto dispatcher = dispatch(can_call<Fs(decltype(args)...)>{}...);
        auto&& f0 = dispatcher(std::get<decltype(Is)::value>(fs_tuple)...);
        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

Perloff answered 3/2, 2017 at 19:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.