8

I have the following type trait:

template <class T>
struct Arity : Arity<decltype(&T::operator())> {};

template <class T, class R, class... Args>
struct Arity<R(T::*)(Args...)> {
    static constexpr auto value = sizeof...(Args);
};

template <class T, class R, class... Args>
struct Arity<R(T::*)(Args...) const> {
    static constexpr auto value = sizeof...(Args);
};

template <class R, class... Args>
struct Arity<R(*)(Args...)>  {
    static constexpr auto value = sizeof...(Args);
};

Which works great to find the number of arguments a function takes for most use cases, but it fails for one common case:

auto l1 = [](int, double){};
Arity<decltype(l1)>::value; // works, 2

auto l2 = [](auto, auto){};
Arity<decltype(l2)>::value; // error: Reference to overloaded function could not be resolved; did you mean to call it?

I believe it's impossible to generally make this work for any templated function / operator() because depending on the types/values passed as template types, a different overload could be selected, or perhaps no overload may be available at all. Also, there's no way to know what valid types and values to pass as template arguments. But still, I want this to work for the common case of a lambda taking auto arguments. Is there any way to make this more robust and cover lambdas which take auto arguments?

David
  • 27,652
  • 18
  • 89
  • 138
  • Is the compilation failure you see a hard failure or SFINAE resulting in no eligible candidates? I think there's a way to count function arguments, but I'm not sure it would nicely autoselect between your current one and the template-lambda capable version. – Ben Voigt Jan 09 '17 at 21:17
  • @BenVoigt `Reference to overloaded function could not be resolved; did you mean to call it?` – David Jan 09 '17 at 21:19
  • Ok so you have a hard error. That makes it a bit more painful for making a solution that works both with- and without- template arguments on `operator()`. – Ben Voigt Jan 09 '17 at 21:20
  • So [this](http://rextester.com/IGPB99972) does work, then it's a matter of adding some template recursion to try increasing numbers of template arguments – Ben Voigt Jan 09 '17 at 21:36
  • @BenVoigt And pass what as arguments? What if the template arguments are values, not types. What if the function is SFINAEd such that if the first type is int, 3 more types must be included, otherwise only 1 type is available, etc. I don't think it's possible, generally – David Jan 09 '17 at 21:41
  • Lambda `operator()` never has non-type template arguments, or SFINAE overloads. It's not a universal solution, but for the case you asked for of lambdas with `auto` args, it should be enough. – Ben Voigt Jan 09 '17 at 21:42
  • @BenVoigt I was speaking for any function template, not just a lambda's operator(). But even to _only_ work for lambda's `operator()` I'd like to disclude non-lambda functions from trying this number of template arguments resolution and afaik there's no way to distinguish a lambda operator() from anyone elses operator(), right? – David Jan 09 '17 at 21:44
  • I think that's a matter of making the termination-of-recursion case (64 arguments or whatever you try) fail with a soft SFINAE and not a hard compile error. – Ben Voigt Jan 09 '17 at 21:45
  • @BenVoigt Wouldn't even instantiating the lambda operator() as operator(), or , or whatever, fail if the lambda was written to only compile if the argument types are ? – David Jan 09 '17 at 21:46
  • gcc gives a clearer error: `error: decltype cannot resolve address of overloaded function struct Arity : Arity {};` . Looking at the `ClosureType::operator()`, it's indeed overloaded. So I guess by (somehow) resolving the overload the issue would be solved. – Danra Jan 09 '17 at 21:48

2 Answers2

4

I guess I achieved half of a solution here. Only works up to a fixed number of parameters, but for most applications that shouldn't be an issue. Also, it's probably highly simplifiable but my brain is not into tricky SFINAE right now.

template <
    class, std::size_t N,
    class = std::make_index_sequence<N>,
    class = void_t<>
>
struct CanCall : std::false_type { };

template <class F, std::size_t N, std::size_t... Idx>
struct CanCall<
    F, N,
    std::index_sequence<Idx...>,
    void_t<decltype(std::declval<F>()((Idx, std::declval<Any const&&>())...))>
> : std::true_type { };

CanCall<F, N> will return whether F is callable with N parameters of arbitrary type. The Any helper type has templated implicit conversion operators that allows it to morph into any desired parameter type.

template <class F, std::size_t N = 0u, class = void>
struct Arity : Arity<F, N + 1u, void> { };

template <class F, std::size_t N>
struct Arity<F, N, std::enable_if_t<CanCall<F, N>::value>>
: std::integral_constant<std::size_t, N> { };

template <class F>
struct Arity<F, MAX_ARITY_PROBING, void>
: std::integral_constant<std::size_t, ARITY_VARIADIC> { };

Arity<F> just checks whether an F can be called with zero, one, two... parameters. First positive check wins. If we reach MAX_ARITY_PROBING parameters, Arity bails out and supposes that the function is either variadic, or is not a function at all.

See it live on Coliru

Quentin
  • 62,093
  • 7
  • 131
  • 191
2

I don't think you can use lambda functions in your use case whose argument types are auto. The operator() functions of such lambda functions are most likely implemented using function templates.

Hence, decltype can't be used with:

auto l2 = [](auto, auto){};
Arity<decltype(l2)>::value;

See this answer to another SO question for more on the subject.

Community
  • 1
  • 1
R Sahu
  • 204,454
  • 14
  • 159
  • 270
  • ya I was pretty sure this was the answer - that it's just not possible. – David Jan 09 '17 at 21:13
  • This is more of a comment than an answer, showing that one approach doesn't work is a long way from proving that no working approach exists. – Ben Voigt Jan 09 '17 at 21:24
  • Also, I think you're analyzing it wrong. `decltype(l2)` is just fine, it's the `&T::operator()` inside of `Arity` that fails. `l2` is not a template (as your untested suggestion assumed), rather it's a non-template class that has a template `operator()`. – Ben Voigt Jan 09 '17 at 21:25