Что должно вернуть tuple_map?

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

  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 (и каковы были бы связанные с ним подводные камни)?

Ответы

Ответ 1

Проблема, которую вы выделили в своем вопросе, заключается в том, что использование std::forward_as_tuple на 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. Демо-версия Live на Coliru