2

I am writing a function probeMaxArgs that deduces the maximum number of arguments in a function/callable object. For simplicity I assume, that the object don't have any overloads of operator() (created from a simple lambda).
I have decided to take an approach where a template function tries to deduce a maximum number of arguments the callable can be invoked with via a probe type which is implicitly convertible to 'anything'.

namespace detail {
namespace {

struct probe
{
    template<typename T>
    consteval operator T const &() const noexcept;

    template<typename T>
    consteval operator T const &&() const noexcept;

    template<typename T>
    consteval operator T&() const noexcept;

    template<typename T>
    consteval operator T&&() const noexcept;
};

template<typename T, size_t N = 10>
    requires(N < 15)
consteval size_t probeMaxArgs()
{
    constexpr auto probeN =
        []<size_t... I>(std::index_sequence<I...>) -> bool
        {
            return std::is_invocable_v<T&,  decltype((void)I, probe{})...> ||
                   std::is_invocable_v<T&&, decltype((void)I, probe{})...>;
        };

    constexpr std::array results =
        [&]<size_t... I>(std::index_sequence<I...>)
        {
            return std::array{probeN(std::make_index_sequence<I>())...};
        }(std::make_index_sequence<N>());

    constexpr size_t result =
        [&]
        {
            size_t r = size_t(-1);
            for (size_t i = 0; i < results.size(); ++i)
                r = results[i] ? i : r;
            return r;
        }();
    static_assert(result != size_t(-1), "Failed to deduce max args");

    return result;
}

} // namespace
} // namespace detail

It works with template lambdas and with default parameters.

    static_assert(0 == detail::probeMaxArgs<decltype([]{})>());
    static_assert(1 == detail::probeMaxArgs<decltype([](auto){})>());
    static_assert(3 == detail::probeMaxArgs<decltype([](auto, auto, auto){})>());
    static_assert(3 == detail::probeMaxArgs<decltype([](auto, auto, auto = 4){})>());

But not with template concepts

        constexpr auto sort = 
            []<
                // std::ranges::random_access_range R, //< doesn't compile
                typename R, //< compiles
                typename Comp = std::ranges::less,
                typename Proj = std::identity>(R&& r, Comp comp = {}, Proj proj = {}) 
                    // -> std::ranges::borrowed_range auto { //< doesn't compile
                {
                // std::ranges::sort(r, comp, proj); //< wtf doesn't compile 
                return std::forward<R>(r);
            };
        static_assert(3 == detail::probeMaxArgs<decltype(sort)>());

If I uncomment any line with a concept, the compilation fails. Also if I uncomment std::ranges::sort AND call prebeMaxArgs, the std::ranges::sort line will fail.

Full code


Another approach, that might be promising:

template <typename F>
struct get {
    template <typename T>
    static constexpr auto op = &F::template operator()<T>;
};

using get_l = get<decltype([]<std::ranges::random_access_range R>(R&& r){})>;

// compiles
template <typename T>
constexpr auto op_l = get_l::op<T>;

// doesn't: only one parameter
// template <typename T, typename T_1>
// constexpr auto op_l = get_l::op<T, T_1>;

However, it will not work with non-type parameters. I suppose, it is possible to try with auto and T, but the combinations will snowball and drastically increase the compilation times.

7
  • 1
    Note that with variadic templates, the number of arguments does not have to be limited. Now, what's the ultimate purpose of deduction of the maximum argument number? How the maximum is supposed to be defined? Commented Jul 28 at 20:33
  • @SergeyAKryukov this a part of closure implementation, which is not a part of the question. The actual implementation has a fallback max_args_t<N> parameter, that can be used in such cases. But I'd like to have a cleaner interface, so the User could utilize concepts. Commented Jul 28 at 20:37
  • You have a specific case to look at, so why not plug it in and see what you can simplify? When you call detail::probeMaxArgs<decltype(sort)>(), you expect the results array to be {false, true, true, true, false, /*rest false*/}; correct? But instead every element is false. So instead of the array, you could focus on one element. results[1] has the simplest calculation, so that's a good choice. That element is set to std::is_invocable_v<decltype(sort)&, decltype(probe{})> || std::is_invocable_v<decltype(sort)&&, decltype(probe{})> so you could focus on this expression instead. Commented Jul 28 at 21:24
  • @JaMiT I suppose you suggest an optimization, that will reduce compile time instantiations. My guess for the actual problem is that detail::probe type simply does not satisfy the concept. Commented Jul 28 at 21:27
  • @SergeyKolesnik "I suppose you suggest an optimization, that will reduce compile time instantiations." -- No, I commented on your question, not on your real code. I suggested an optimization that will make your question simpler, applicable to more situations, and easier to answer. You could relegate probeMaxArgs to context and instead ask why std::is_invocable_v<decltype(sort)&, decltype(probe{})> is false (i.e. why you cannot invoke sort(detail::probe{})) when using a concept. Commented Jul 28 at 21:37

2 Answers 2

1

Deduce maximum number of function arguments

I don't see a way in the generic case.

probe's trick is indeed a way which work for specific types.

It also work with (simple) auto by using probe type directly (no conversions).

"It works [..] But not with template concepts"

Indeed probe doesn't fulfil the concept. It would also failed for non-deducible conversions, such as template <typename T> void foo(std::vector<T>).

"For simplicity I assume, that the object don't have any overloads of operator()"

template is a kind of overload set though...

Another approach [..]

That would only count template parameters, not function parameter, consider template <typename T, template Allocator> void bar(std::vector<T, Alloc>).

Maybe reflection of C++26 might help...

Sign up to request clarification or add additional context in comments.

3 Comments

Very on point, thank you. I haven't consider the template specializations like vector, which is a legit use case. I will give it a little bit of thought though
I had another thought that could address the non-deducible conversions in general: probing instantiations with a template template argument. That can deduce the number of template arguments for a function. After that, one can actually get the number of function arguments without resorting to implicit conversions with probe. However, I don't think it should work according to the standard. template <typename> is not the same as template <SomeConcept>. But for some reason, such code in my update compiles...
0

The number of arguments a function takes can be the result of a Turing-complete computation in C++.

For example, I could write a function that takes any number of arguments, so long as the collatz conjecture holds for each of the argument numbers in question. Or, some other easy to check property that has no known inversion.

You cannot invert a Turing-complete computation in general; that is equivalent to Halt and/or Rice's Theorem.

template<std::size_t...Is>
struct insanity {
  template< class... Ts>
  requires (true && some_test_v<Is, Ts>...)
  void operator()( Ts... ts );
};

how many arguments this can take is determined by whether some_test_v<N, ?> can be passed for any argument ?. A simple case like

template<std::size_t I, class T>
constexpr std::integral_constant<bool, (I < 2)> some_test_v = {};

means that 0 or 1 arguments are allowed to be passed to insanity{}, but the computation I < 2 could be of arbitrary complexity. (ie, if I is 3, check if T encodes a valid proof that P=NP)

In short, what you are asking to do cannot be done by the very laws of mathematics because C++'s "how many arguments does a function take" is a problem that is too hard to solve by mathematics, let alone your technique.

At best what you can do is hack around it and get a partial answer.

When reflection arrives, you'll have a different partial answer; nothing will be able to answer the question for types like insanity. C++ gave too much power to the code generation capabilities of the language for every question to have an answer.

In your case, you are just asking "does there exist a type that can be passed to this argument"; that is what your probe type does. And that question cannot be answered in general.

Now, the C++ language does require that all templates have a valid instantiation, and that a template with a pack must have a valid instantiation with a non-empty pack; so theoretically the language could provide you with a crude approximation of what you want (less crude than your solution, as your solution requires actually finding the types, and the language can do away with that problem and presume they exist). It would remain crude, as determining if your function can take 2 arguments can be impossible mathematically.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.