Я хочу реализовать общую функцию 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>>>{});
}
В случае 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
(и каковы были бы связанные с ним подводные камни)?