元组映射函数应该返回什么?

12
我想实现一个通用的tuple_map函数,它接受一个函数对象和一个std::tuple,将该函数应用于元组的每个元素,并返回结果的std::tuple。实现非常简单,但问题是:这个函数应该返回什么类型?我的实现使用了std::make_tuple。然而,这里建议使用std::forward_as_tuple
具体来说,实现如下(为简洁起见,省略了处理空元组的部分):
#include <cstddef>
#include <tuple>
#include <type_traits>
#include <utility>

template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_v(Fn fn, Tuple&& tuple, std::index_sequence<indices...>)
{
    return std::make_tuple(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
    //          ^^^
}

template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple, std::index_sequence<indices...>)
{
    return std::forward_as_tuple(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
    //          ^^^
}

template<class Tuple, class Fn>
constexpr auto tuple_map_v(Fn fn, Tuple&& tuple)
{ 
    return tuple_map_v(fn, std::forward<Tuple>(tuple), 
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

template<class Tuple, class Fn>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple)
{ 
    return tuple_map_r(fn, std::forward<Tuple>(tuple), 
        std::make_index_sequence<std::tuple_size_v<std::remove_reference_t<Tuple>>>{});
}

在第一种情况下,我们使用std::make_tuple,它会衰减每个参数的类型(对于值使用_v),而在第二种情况下,我们使用std::forward_as_tuple,它会保留引用(对于引用使用_r)。这两种情况都有其优缺点。
  1. Dangling references.

    auto copy = [](auto x) { return x; };
    auto const_id = [](const auto& x) -> decltype(auto) { return x; };
    
    auto r1 = tuple_map_v(copy, std::make_tuple(1));
    // OK, type of r1 is std::tuple<int>
    
    auto r2 = tuple_map_r(copy, std::make_tuple(1));
    // UB, type of r2 is std::tuple<int&&>
    
    std::tuple<int> r3 = tuple_map_r(copy, std::make_tuple(1));
    // Still UB
    
    std::tuple<int> r4 = tuple_map_r(const_id, std::make_tuple(1));
    // OK now
    
  2. Tuple of references.

    auto id = [](auto& x) -> decltype(auto) { return x; };
    
    int a = 0, b = 0;
    auto r1 = tuple_map_v(id, std::forward_as_tuple(a, b));
    // Type of r1 is std::tuple<int, int>
    ++std::get<0>(r1);
    // Increments a copy, a is still zero
    
    auto r2 = tuple_map_r(id, std::forward_as_tuple(a, b));
    // Type of r2 is std::tuple<int&, int&>
    ++std::get<0>(r2);
    // OK, now a = 1
    
  3. Move-only types.

    NonCopyable nc;
    auto r1 = tuple_map_v(id, std::forward_as_tuple(nc));
    // Does not compile without a copy constructor 
    
    auto r2 = tuple_map_r(id, std::forward_as_tuple(nc));
    // OK, type of r2 is std::tuple<NonCopyable&>
    
  4. References with std::make_tuple.

    auto id_ref = [](auto& x) { return std::reference_wrapper(x); };
    
    NonCopyable nc;
    auto r1 = tuple_map_v(id_ref, std::forward_as_tuple(nc));
    // OK now, type of r1 is std::tuple<NonCopyable&>
    
    auto r2 = tuple_map_v(id_ref, std::forward_as_tuple(a, b));
    // OK, type of r2 is std::tuple<int&, int&>
    

可能我理解错误或者漏掉了一些重要的东西。

看起来make_tuple是正确的选择:它不会产生悬空引用,同时仍然可以强制推导出引用类型。您将如何实现tuple_map(以及与之相关的陷阱)?


有趣的问题。但是写调用的人不应该负责确保行为定义良好吗?例如,如果我们采用auto fn = [](auto const& x) { return std::cref(x); }; auto a = fn(2);,我们将得到一个悬空引用,而没有任何元组参与。我的结论是使用forward_as_tuple,知道(小)注意事项。 - Rerito
你是否考虑过为左值和右值制作单独的重载/SFINAE版本?我认为这将允许你同时获得两全其美。 - bartop
1个回答

6
您在问题中提到的问题是,对于返回值的函数使用std::forward_as_tuple会导致您在生成的元组中得到一个rvalue引用。
通过使用make_tuple,您不能保留lvalue-refs,但是通过使用forward_as_tuple,您不能保留普通值。您可以依靠std::invoke_result来找出结果元组必须包含的类型,并使用适当的std::tuple构造函数。
template<class Fn, class Tuple, std::size_t... indices>
constexpr auto tuple_map_r(Fn fn, Tuple&& tuple, std::index_sequence<indices...>) {
    using tuple_type = std::tuple<
        typename std::invoke_result<
            Fn, decltype(std::get<indices>(std::forward<Tuple>(tuple)))
        >::type...
    >;
    return tuple_type(fn(std::get<indices>(std::forward<Tuple>(tuple)))...);
}

这样可以保留对fn调用结果的值类别。

在Coliru上实时演示

通过模板参数推导,我理解无需使用 tuple_type,直接使用 return std::tuple(fn(...)...) 即可。我本来想在我的问题中包含这个替代方案,但是(错误地)认为它与 forward_as_tuple 等效。 - Evg
@Evgeny 正确。我认为使用模板推导指南可以解决这个问题(虽然我还不太自信,因为我还不太熟悉它们...)。 - Rerito
1
@Evgeny 经过一些测试,似乎std::tuple的推导指南会使它们的参数衰减...因此,显式类型仍然是唯一的选择! - Rerito
1
我刚刚进行了类似的测试,本来想写一条评论说 std::tuple(...) 并不真正起作用。 :) - Evg
4
tuple(x...) 相当于 make_tuple(x...),但区别在于 reference_wrapper<T> 会保留为 reference_wrapper<T>,而不会变成 T& - Barry

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