【发布时间】: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 用于引用)。这两种情况各有利弊。
-
悬空引用。
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 -
引用元组。
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 -
仅移动类型。
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&> -
引用
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知道(小)警告 -
您是否想过为左值和右值制作单独的重载/SFINAE 版本?我认为这可以让你两全其美