7
\$\begingroup\$

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.

\$\endgroup\$
0

2 Answers 2

10
\$\begingroup\$

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.

\$\endgroup\$
1
  • \$\begingroup\$ Hm. std::get<0>(declval(T)) can be undefined and the type still tuple-like, if std::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\$ Jun 29, 2022 at 19:21
3
\$\begingroup\$

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 ..
  } ;
\$\endgroup\$
1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.