无法对用户定义的类型使用 std::apply

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

在为我正在进行的某个项目实施 compressed_tuple class 时,我 运行 遇到以下问题:我似乎无法将此类型的实例传递给std::apply,尽管根据 https://en.cppreference.com/w/cpp/utility/apply.

这应该是可能的

我使用以下片段 (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);

当我尝试编译这段代码时,它似乎找不到我定义的 std::get 重载,即使它们应该完全匹配。相反,它会尝试匹配所有其他重载(std::get(pair)、std::get(array<...>) 等),甚至不提及我的重载.我在所有三个主要编译器(MSVC、Clang、GCC)中都遇到了一致的错误。

所以我的问题是这是否是预期的行为并且根本不可能将 std::apply 用于用户定义的类型?有解决办法吗?

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

不行,目前没有办法。

在 libstdc++、libc++ 和 MSVC-STL 实现中,std::apply 在内部使用 std::get 而不是不合格的 get,因为禁止用户在 [ 下定义 get =14=],无法将 std::apply 应用于用户自定义类型。

你可能会问,在[tuple.creation]中,标准是这样描述tuple_cat的:

[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]

这是否表明其他 tuple 实用函数(例如 std::apply 应该支持用户定义的 tuple 类类型?

请特别注意,术语“tuple-like”在 这个时间点。这是故意留给 C++ 委员会的,以弥补这一差距 由未来的提案填补。有一个提案是 打算着手改进这件事,见P2165R3.


And is there a work-around?

在采用 P2165 之前,不幸的是,您可能必须实现自己的 apply 并使用不合格的 get

问题已被@康桐薇完整解答。我将 post 有关如何提供解决方法的更多详细信息。

首先,这是一个通用的 C++20 apply 函数:

#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

自己的命名空间很有用,这样自定义应用就不会干扰 std-version。对 get 的不合格调用意味着(引用他的 blog 中的@Quuxplusone,这是迄今为止我遇到的最好的解释):

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.”

然后您可以推出自己的 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;
    };
}

通过 get() 的重载和 std::tuple_size 的特化,应用函数将按预期工作。此外,您可以插入任何兼容的 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