2

I would like the ability to disable a function if a callable template argument is not callable via std::apply.

Perhaps it would help to explain the problem starting with what works. I have this for the regular function call case:

template <class Fn, class... Args>
using enable_if_callable = decltype(std::declval<Fn>()(std::declval<Args>()...));

...

template <class Fn, class = enable_if_callable<Fn, int, float>>
void call_fn(Fn fn) {
  fn(1, 2.0f);
}

// The above is correctly disabled in this case:
call_fn([](int) {})

The above works well to disable call_fn when fn can't be called with arguments (int, float). I only get one error at the site of instantiation, as intended.

I'm trying to find an equivalent for disabling a function if the Args are specified in an std::tuple, for use with std::apply:

template <class Fn, class TupleArgs>
using enable_if_applicable = 
    decltype(std::apply(std::declval<Fn>(), std::declval<TupleArgs>()));

...

template <class Fn, class = enable_if_applicable<Fn, std::tuple<int, float>>>
void apply_fn(Fn fn) {
  std::apply(fn, std::make_tuple(1, 2.0f));
}

// The above is not correctly disabled in this case:
apply_fn([](int) {})

This doesn't prevent instantiation, at least not to avoid errors from inside apply, presumably because these errors are not from substitution failures of the declaration. The techniques I'm familiar with for peeling apart tuples don't seem to apply here (index sequence, etc.).

Of course, in this non-general example, just using enable_if_callable<Fn, int, float> would work, but I'm trying to make this work when the tuple could have any number of elements.

Any ideas on how to implement such a thing?

dsharlet
  • 1,036
  • 1
  • 8
  • 15
  • Thanks for looking. I tried to rephrase slightly, and tweaked the example functions to be concrete instead of generic, to hopefully make it easier to understand. – dsharlet Jul 20 '20 at 03:32

3 Answers3

1

You can use std::is_invocable from <type_traits> as follows

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

template <class Fn, class ... Args, class = std::enable_if_t<std::is_invocable_v<Fn, Args...>>>
void apply_fn(Fn&& fn, const std::tuple<Args...>& t) {
    std::apply(std::forward<Fn>(fn), t);
}

int main() {
    apply_fn([ ](int el1,float el2) {std::cout << el1 << " " << el2;}, std::make_tuple(1,2.3f));//compiles
    //apply_fn([](int ) {},std::make_tuple(1,2.3f));//doesn't compile
}

Live

If you need to make your function be restricted to an internal tuple as the comment says, you can use the following two templates together instead

template <class Fn, class ... Args, class = std::enable_if_t<std::is_invocable_v<Fn, Args...>>>
void helper(Fn&& fn, const std::tuple<Args...>& t) {
    std::apply(std::forward<Fn>(fn), t);
}

template <class Fn>
void apply_fn(Fn&& fn) {
    helper(std::forward<Fn>(fn), std::make_tuple(1,2.3f));
}

Live

asmmo
  • 6,922
  • 1
  • 11
  • 25
  • This only works if you have access to `class ... Args` (i.e. the tuple is part of the template declaration). It's equivalent to what my `enable_if_callable` does. Basically, what I'm looking for is something that works if the parameter to the enable_if expression is the whole tuple type (`T = std::tuple`) rather than the tuple elements (`Args... = Ts...`). I did find a solution, which was to make an `apply` that makes the return type part of the declaration. – dsharlet Jul 20 '20 at 06:34
  • This generates an error one level removed from the API though (`helper`, not `apply_fn`) and `apply_fn` would erroneously participate in overload resolution. My goal is to make it so when people incorrectly use this API, the error they get is in their code, not in my header. – dsharlet Jul 20 '20 at 08:01
0

Of course, I figured out the solution a few hours after posting. The key was to implement my own version of apply which uses a trailing return type, which causes the template substitution to fail, instead of the instantiation.

template <class Fn, class Args, size_t... Is>
auto apply(Fn&& fn, const Args& args, index_sequence<Is...>)
    -> decltype(fn(std::get<Is>(args)...)) {
  return fn(std::get<Is>(args)...);
}
template <class Fn, class... Args>
auto apply(Fn&& fn, const std::tuple<Args...>& args)
    -> decltype(apply(fn, args, make_index_sequence<sizeof...(Args)>())) {
  return apply(fn, args, make_index_sequence<sizeof...(Args)>());
}

template <class Fn, class Args>
using enable_if_applicable = decltype(apply(std::declval<Fn>(), std::declval<Args>()));
dsharlet
  • 1,036
  • 1
  • 8
  • 15
0

There is no need to reinvent the wheel. Just use what @asmmo said:

template <class Fn, class ... Args, std::enable_if_t<std::is_invocable_v<Fn, Args...>, int> = 0>
void apply_fn(Fn&& fn, const std::tuple<Args...>& t) {
    std::apply(std::forward<Fn>(fn), t);
}

template<class Fn, class Tuple>
using enable_if_applicable = decltype(apply_fn(std::declval<Fn>(), std::declval<Tuple>()));
Mestkon
  • 3,532
  • 7
  • 18
  • This is a good solution, though it's not quite the same as @asmmo's. However, in my case, I'm targeting C++14, so I need my own implementation of `apply` anyways :) (I did check that `std::apply` with C++17 didn't work in this case either though, because it does not have the trailing return type either). – dsharlet Jul 20 '20 at 08:05