【问题标题】:What should tuple_map return?tuple_map 应该返回什么?
【发布时间】:2019-01-14 14:43:29
【问题描述】:

我想实现一个通用的tuple_map 函数,它接受一个函子和一个std::tuple,将函子应用于这个元组的每个元素并返回一个std::tuple 的结果。实现非常简单,但是问题出现了:这个函数应该返回什么类型?我的实现使用了std::make_tuple。不过建议herestd::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>>>{});
}

在情况 1 中,我们使用 std::make_tuple 衰减每个参数的类型(_v 用于值),在情况 2 中,我们使用 std::forward_as_tuple 保留引用(_r 用于引用)。这两种情况各有利弊。

  1. 悬空引用。

    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. 引用元组。

    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. 仅移动类型。

    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. 引用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&amp; x) { return std::cref(x); }; auto a = fn(2);,我们将得到一个悬空引用,而不涉及任何元组。我的结论是选择forward_as_tuple 知道(小)警告
  • 您是否想过为左值和右值制作单独的重载/SFINAE 版本?我认为这可以让你两全其美

标签: c++ c++17 stdtuple


【解决方案1】:

您在问题中强调的问题是,在按值返回的仿函数上使用 std::forward_as_tuple 会在结果元组中留下一个右值引用。

使用make_tuple 不能保留左值引用,但是使用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 调用结果的值类别。 Live demo on Coliru

【讨论】:

  • 我是否更正了模板参数推导我们不需要tuple_type而可以简单地return std::tuple(fn(...)...)?我什至想将此替代方案包含在我的问题中,但(错误地)认为它等同于forward_as_tuple
  • @Evgeny 正确。使用模板推导指南,我认为它会起作用(我不太自信,因为我还不熟悉它们......)。
  • @Evgeny 在testing 之后,似乎std::tuple 的推导指南衰减了他们的论点......所以显式类型似乎仍然是唯一的方法!
  • 我刚刚做了一个类似的测试,并打算写一个评论 std::tuple(...) 并没有真正起作用。 :)
  • @Evgeny tuple(x...) 等同于make_tuple(x...),除了reference_wrapper&lt;T&gt; 保持为reference_wrapper&lt;T&gt; 并且不会变为T&amp;
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-04
  • 2017-12-12
  • 2011-03-09
相关资源
最近更新 更多