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.
closure
implementation, 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 theresults
array 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::probe
type simply does not satisfy the concept.probeMaxArgs
to 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.