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.
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.
closureimplementation, which is not a part of the question. The actual implementation has a fallbackmax_args_t<N>parameter, that can be used in such cases. But I'd like to have a cleaner interface, so the User could utilizeconcepts.detail::probeMaxArgs<decltype(sort)>(), you expect theresultsarray to be{false, true, true, true, false, /*rest false*/};correct? But instead every element isfalse. 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 tostd::is_invocable_v<decltype(sort)&, decltype(probe{})> || std::is_invocable_v<decltype(sort)&&, decltype(probe{})>so you could focus on this expression instead.detail::probetype simply does not satisfy the concept.probeMaxArgsto context and instead ask whystd::is_invocable_v<decltype(sort)&, decltype(probe{})>is false (i.e. why you cannot invokesort(detail::probe{})) when using a concept.