无法在用户定义类型上使用std::apply。

5
在我正在进行的某个项目中实现 compressed_tuple 类时,遇到了以下问题:即使根据https://en.cppreference.com/w/cpp/utility/apply,似乎无法将此类型的实例传递给 std::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<T,U>),std :: get(array<...>)等),而没有提及我的重载。我在所有三个主要编译器(MSVC,Clang,GCC)中都得到一致的错误。
所以我的问题是,是否可以预期出现这种行为,并且无法使用用户定义类型与std::apply一起使用?是否有解决方法?

2
你的行为未定义。不允许向std添加函数重载。https://en.cppreference.com/w/cpp/language/extending_std - NathanOliver
@NathanOliver,你是对的。看起来我并没有完全理解有关扩展std命名空间的规则。我一直以为你不能添加新的实体,但可以重载现有的。显然我错了。 - Erik Valkering
2个回答

8
所以我的问题是这是否是预期行为,无法使用std::apply与用户定义的类型?
不,目前没有办法。
在libstdc++、libc++和MSVC-STL实现中,std::apply在内部使用std::get而不是未经限定的get,因为用户被禁止在namespace std下定义get,所以无法将std::apply应用于用户定义的类型。
您可以问,在元组创建中,标准描述了tuple_cat如下:
[注意1:实现可以支持模板参数包Tuples中支持tuple-like协议的其他类型,例如pair和array。——结束注释]
这是否意味着其他类似于tuple的实用函数(例如std::apply)应支持用户定义的类似于tuple的类型?
需要注意的是,特别是术语“tuple -like”在此时没有具体的定义。这是故意留给C ++委员会,以便未来的提案填补这一差距。有一个提案即将开始改进这个问题,请参见P2165R3

那有没有解决方法?

在采用P2165之前,不幸的是,您可能需要实现自己的apply并使用非限定的get


可能你去年发布的第一个链接已经更新了,但是现在我没有看到get前面有任何std::限定符。不过问题仍然存在,即标准库提供者是否也应用了这个更新。 - davidhigh
1
@davidhigh https://github.com/cplusplus/draft/issues/4948 - 康桓瑋
谢谢,这解释了在 get 之前省略 std:: 的原因。不过,它似乎对任何编译器都不起作用:(https://godbolt.org/z/noo5hb8Pe)[https://godbolt.org/z/noo5hb8Pe] - davidhigh
1
这只是一个编辑修复。由于P2165尚未被采纳,因此标准仍不要求支持用户定义的类似元组的类型。 - 康桓瑋

1

@康桓瑋已经彻底回答了这个问题。我将发布一些有关如何提供解决方法的更多细节。

首先,这是一个通用的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版本。对get的未限定调用意味着(引用@Quuxplusone在他的博客中的话,这是我至今遇到的最好的解释):

使用两步骤的未限定调用,比如使用my::xyzzy; xyzzy(t), 表示“我知道一种xyzzy任何东西的方法,但T本身可能知道更好的方法。如果T有一个观点,你应该相信T而不是我。”

然后您可以推出自己的类似元组的类,

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的特化,apply函数可以按预期工作。此外,您可以插入任何符合标准的std类型:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接