Unable to use std::apply on user-defined types
Asked Answered
V

2

6

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?

Valedictorian answered 17/9, 2021 at 1:56 Comment(2)
You have undefined behavior. You are not allowed to add function overloads to std. en.cppreference.com/w/cpp/language/extending_stdPiddling
@NathanOliver, you're right. It seems that I don't fully understand the rules regarding extending the std namespace. I always assumed you were not allowed to add new entities, but were allowed to overload existing ones. Apparently I was wrong.Valedictorian
V
8

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.

Vocalic answered 17/9, 2021 at 3:43 Comment(4)
It might be that the first link you posted has updated since last year, but now I don't see any std:: qualification before get. Still there is the question, whether the stdlib-providers have applied this as well.Weariful
@Weariful github.com/cplusplus/draft/issues/4948Cupulate
Thanks, this explains the omission of std:: before get. Anyways, it doesn't seem to work for any compiler: (godbolt.org/z/noo5hb8Pe)[https://godbolt.org/z/noo5hb8Pe]Weariful
That's just an editorial fix. Since P2165 has not been adopted, the standard still does not require support for user-defined tuple-like types.Cupulate
W
1

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

Weariful answered 7/5, 2022 at 14:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.