Search code examples
c++c++14template-meta-programmingoverload-resolution

How to extend std::apply to work on non tuple types?


I have a case where I need to apply the input argument to a function without caring if it is a tuple or not. If it is a tuple, it needs to be unpacked, so no function argument detection is needed.

Here is what I have tried:

template <typename Callable, typename Tuple>
auto geniune_apply(Callable&& callable, Tuple&& tuple)
{
    return std::apply(std::forward<Callable>(callable), std::forward<Tuple>(tuple));
}

template <typename Callable, typename T, typename = typename std::enable_if<!shino::is_tuple_like<std::decay_t<T>>::value>::type>
auto geniune_apply(Callable&& callable, T&& arg)
{
    return std::forward<Callable>(callable)(std::forward<T>(arg));
}

it results in ambiguity, which is what I expected. Then I tried to SFINAE on the size of the tuple instead, but I couldn't prevent compilation error on non tuple types.

Here are the test cases I'm using:

#include <cassert>
#include <iostream>
#include <stdexcept>
#include <vector>

int dummy_x(const std::tuple<int, int>&)
{
    return 1;
}

int dummy_y(int y)
{
    return y;
}

int main()
{
    shino::geniune_apply(&dummy_x, std::tuple<int, int>(1, 1));
    shino::geniune_apply(dummy_y, 1);
    shino::geniune_apply(dummy_y, std::make_tuple(1));
}

Code for tuple-like, if needed. It basically tests if it is std::array or std::tuple:

template <typename T>
    struct is_straight_tuple
    {
        static constexpr bool value = false;

        constexpr operator bool()
        {
            return value;
        }
    };

    template <typename ... Ts>
    struct is_straight_tuple<std::tuple<Ts...>>
    {
        static constexpr bool value = true;

        constexpr operator bool()
        {
            return value;
        }
    };

    template <typename T>
    struct is_std_array
    {
        static constexpr bool value = false;
    };

    template <typename T, std::size_t size>
    struct is_std_array<std::array<T, size>>
    {
        static constexpr bool value = true;

        constexpr operator bool()
        {
            return value;
        }
    };

    template <typename T>
    struct is_tuple_like
    {
        static constexpr bool value = is_std_array<T>::value || is_straight_tuple<T>::value;

        constexpr operator bool()
        {
            return value;
        }
    };

Solution

  • The simplest way to solve this in C++14 is to just use tag-dispatch:

    template <typename Callable, typename Arg>
    decltype(auto) geniune_apply(Callable&& callable, Arg&& arg)
    {
        return details::genuine_apply(std::forward<Callable>(callable),
             std::forward<Arg>(arg),
             is_tuple_like<std::decay_t<Arg>>{});
    }
    

    Change your is_tuple_like to inherit from a std::integral_constant<bool, ???> instead of reimplementing the same. This would allow you to then write these two helper functions:

    namespace details {
        // the tuple-like case
        template <typename Callable, typename Tuple>
        decltype(auto) genuine_apply(Callable&&, Tuple&&, std::true_type );
    
        // the non-tuple-like case
        template <typename Callable, typename Arg>
        decltype(auto) genuine_apply(Callable&&, Arg&&, std::false_type );
    }
    

    In C++17, the way nicer solution is to simply use if constexpr instead of tag dispatching. With C++ Concepts, your initial approach to the problem would actually work as is (have one unconstrained function template, and one constrained on the second argument being tuple-like).