Unable to use std::apply on user-defined types

620 views Asked by At

While implementing a compressed_tuple class for some project I'm working on, I ran into the following issue: I can't seem to pass instances of this type to std::apply, even though this should be possible according to: https://en.cppreference.com/w/cpp/utility/apply.

I managed to reproduce the issue quite easily, using the following fragment (godbolt):

#include <tuple>

struct Foo {
public:
    explicit Foo(int a) : a{ a } {}

    auto &get_a() const { return a; }
    auto &get_a() { return a; }

private:
    int a;
};

namespace std {

template<>
struct tuple_size<Foo> {
    constexpr static auto value = 1;
};

template<>
struct tuple_element<0, Foo> {
    using type = int;
};

template<size_t I>
constexpr auto get(Foo &t) -> int & {
    return t.get_a();
}

template<size_t I>
constexpr auto get(const Foo &t) -> const int & {
    return t.get_a();
}

template<size_t I>
constexpr auto get(Foo &&t) -> int && {
    return std::move(t.get_a());
}

template<size_t I>
constexpr auto get(const Foo &&t) -> const int && {
    return move(t.get_a());
}

} // namespace std

auto foo = Foo{ 1 };
auto f = [](int) { return 2; };

auto result = std::apply(f, foo);

When I try to compile this piece of code, it seems that it cannot find the std::get overloads that I have defined, even though they should perfectly match. Instead, it tries to match all of the other overloads (std::get(pair<T, U>), std::get(array<...>), etc.), while not even mentioning my overloads. I get consistent errors in all three major compilers (MSVC, Clang, GCC).

So my question is whether this is expected behavior and it's simply not possible to use std::apply with user-defined types? And is there a work-around?

2

There are 2 answers

4
康桓瑋 On BEST ANSWER

So my question is whether this is expected behavior and it's simply not possible to use std::apply with user-defined types?

No, there is currently no way.

In libstdc++, libc++, and MSVC-STL implementations, std::apply uses std::get internally instead of unqualified get, since users are prohibited from defining get under namespace std, it is impossible to apply std::apply to user-defined types.

You may ask, in [tuple.creation], the standard describes tuple_cat as follows:

[Note 1: An implementation can support additional types in the template parameter pack Tuples that support the tuple-like protocol, such as pair and array. — end note]

Does this indicate that other tuple utility functions such as std::apply should support user-defined tuple-like types?

Note that in particular, the term "tuple-like" has no concrete definition at this point of time. This was intentionally left C++ committee to make this gap being filled by a future proposal. There exists a proposal that is going to start improving this matter, see P2165R3.


And is there a work-around?

Before P2165 is adopted, unfortunately, you may have to implement your own apply and use non-qualified get.

0
davidhigh On

The question has throughly been answered by @康桓瑋. I'm going to post some more details on how to provide a workaround.

First, here is a generic C++20 apply function:

#include<tuple>
#include<functional>

namespace my
{
    constexpr decltype(auto) apply(auto&& function, auto&& tuple)
    {
        return []<size_t ... I>(auto && function, auto && tuple, std::index_sequence<I...>)
        {
            using std::get;
            return std::invoke(std::forward<decltype(function)>(function)
                             , get<I>(std::forward<decltype(tuple)>(tuple)) ...);
        }(std::forward<decltype(function)>(function)
        , std::forward<decltype(tuple)>(tuple)
        , std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<decltype(tuple)> > >{});
    }
} //namespace my

The own namespace is useful so that the custom apply does not interfere with the std-version. The unqualified call to get means (quoting @Quuxplusone from his blog, which gives the best explanation I encountered so far):

An unqualified call using the two-step, like using my::xyzzy; xyzzy(t), indicates, “I know one way to xyzzy whatever this thing may be, but T itself might know a better way. If T has an opinion, you should trust T over me.”

You can then roll out your own tuple-like class,

struct my_tuple
{
    std::tuple<int,int> t;   
};

template<size_t I>
auto get(my_tuple t)
{
    return std::get<I>(t.t);
}

namespace std
{
    template<>
    struct tuple_size<my_tuple>
    {
        static constexpr size_t value = 2;
    };
}

With the overload of get() and the specialization of std::tuple_size, the apply function then works as expected. Moreover, you can plug in any compliant std-type:

int main()
{
    auto test = [](auto ... x) { return 1; };

    my::apply(test, my_tuple{});
    my::apply(test, std::tuple<int,double>{});    
    my::apply(test, std::pair<int,double>{});    
    my::apply(test, std::array<std::string,10>{});    
}

DEMO