Motivation:
std::apply
takes single tuple like argument and unpacks the tuple into the function call. The limitation is that one has to know if the argument is tuple like or not, which makes its use cases severed.
I found a way to solve the problem having 100% backwards compatibility (at least I believe so). The code below didn't break any of the benchmarks I've written before, using benchmark v2, so it is tested:
Code:
template <typename T>
struct is_straight_tuple : public std::false_type
{};
template <typename ... Ts>
struct is_straight_tuple<std::tuple<Ts...>> : public std::true_type
{};
template <typename T>
struct is_std_array : public std::false_type
{};
template <typename T, std::size_t size>
struct is_std_array<std::array<T, size>> : public std::true_type
{};
template <typename T>
struct is_tuple_like : public std::bool_constant<is_std_array<T>::value || is_straight_tuple<T>::value>
{};
namespace detail
{
template <typename Callable, typename Tuple>
decltype(auto) genuine_apply(Callable&& callable, Tuple&& tuple, std::true_type)
{
return std::apply(std::forward<Callable>(callable), std::forward<Tuple>(tuple));
}
template <typename Callable, typename Tuple>
decltype(auto) genuine_apply(Callable&& callable, Tuple&& tuple, std::false_type)
{
return std::forward<Callable>(callable)(std::forward<Tuple>(tuple));
}
}
template <typename Callable, typename T>
decltype(auto) genuine_apply(Callable&& callable, T&& argument)
{
return detail::genuine_apply(std::forward<Callable>(callable), std::forward<T>(argument),
is_tuple_like<std::decay_t<T>>{});
};
The usage is the same as of std::apply<>()
(callables
is a tuple of functions to be benchmarked):
auto callable_input = gen(input); //separate input creation from benchmark
auto start = std::chrono::high_resolution_clock::now();
shino::genuine_apply(std::get<Index>(callables), callable_input);
auto end = std::chrono::high_resolution_clock::now();
Some simpler tests:
#include <stdexcept>
int dummy_x(const std::tuple<int, int>&)
{
return 1;
}
int dummy_y(int y)
{
return y;
}
void check(int retvalue, int expectedvalue)
{
if (retvalue != expectedvalue)
{
throw std::logic_error("genuine apply doesn't return the correct value");
}
}
int main()
{
int res = 0;
res = shino::genuine_apply(dummy_x, std::make_tuple(std::tuple<int, int>(1, 1)));
check(res, 1);
res = shino::genuine_apply(dummy_y, 1);
check(res, 1);
shino::genuine_apply(dummy_y, std::make_tuple(1));
check(res, 1);
}
The solution is based on this SO question.
2 Answers 2
I think there are two flaws with this function; the first is in conception, and the second is in implementation.
The conception problem is this: What does genuine_apply
really do? You pass it a callable and an argument. If the argument is a tuple-like object, you expand it and pass its elements as separate arguments to the callable. If you pass it a non-tuple-like argument, you pass the argument directly to the callable. What are the circumstances in which you want that behavior? Think of apply
as an aid to generic programmers; they know whether they're passing a tuple-like argument to apply
because they construct the argument. Typically, you wouldn't pass user-provided arguments directly to apply
, you'd construct the argument yourself from the user-provided arguments. Here, you'll get different behavior based on whether the user passes a vector
or a tuple
. In short, it makes it very difficult for a reader of your code to understand exactly what is going to happen.
The error in implementation is that is_tuple_like
is incorrect. It does not accept std::pair
nor any user-defined type or future standard type that supports std::get
and std::tuple_size
. Presumably instead of checking for membership in a list of types, you should check whether std::get<0>(declval(T))
and std::tuple_size<T>::value
are defined.
-
\$\begingroup\$ Hm.
std::get<0>(declval(T))
can be undefined and the type still tuple-like, ifstd::tuple_size<T>::value
is zero. The thorough and proper test is whether the size is defined, and all elements expected can be retrieved. \$\endgroup\$Deduplicator– Deduplicator2022年06月29日 19:21:34 +00:00Commented Jun 29, 2022 at 19:21
Hm, ok ... spending time on requirements is a step one can not jump over.
std::apply
is a generic solution to a function call to the unknown function with zero or more arguments of "anything". (and yes 'ruds' was also right about std::pair
not being handled by is_tuple_like
) ...
One can possibly write some utilities to aid particular use cases. Like (for example) passing arrays, init lists, and such. Which again will result in transformations to tuples, not changes of the std::apply
.
As a quick sketch maybe something like this (not a flawless C++):
struct apply_helper final {
// static assert if F is Callable
// might go in there
// apply the native array
template< typename F, typename T, size_t N>
auto operator ()
( F invocable_, const T(&array_)[N]) ;
// apply the init list
template <typename F, typename T>
auto operator ()
( F invocable_, std::initializer_list<T> && initlist_ ) ;
// and so on ..
} ;
-
\$\begingroup\$ dbj.org/c-stdapply-made-usable \$\endgroup\$DBJDBJ– DBJDBJ2022年07月08日 09:24:10 +00:00Commented Jul 8, 2022 at 9:24
Explore related questions
See similar questions with these tags.