Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Types of elements of a tuple view

I'm designing a utility to create a view into a tuple-like object. The goal is to enable writing code like this:

// make_tuple_view<Is...>(t): view of t's elements at positions Is...

auto t = std::make_tuple(42, 'c', 3.14);
auto tv = make_tuple_view<2, 1, 0, 1>(t);  // tv == (3.14, 'c', 42, 3.14)
++std::get<0>(tv);
// tv == (4.14, 'c', 42, 4.14)
// t  == (42, 'c', 4.14)

So the element types of a tuple view are appropriate types of references to the corresponding elements in the original tuple. Currently this is how I deduce the reference types:

#include <tuple>
#include <utility>

namespace detail {

template<std::size_t FromIndex, std::size_t... Is, typename Tuple>
constexpr auto make_tuple_range_impl(std::index_sequence<Is...>,
                                     Tuple&& t) noexcept
{
    return std::forward_as_tuple(
            std::get<FromIndex + Is>(std::forward<Tuple>(t))...);
}

}  // namespace detail

// make_tuple_range
template<std::size_t FromIndex, std::size_t ToIndex, typename Tuple>
constexpr auto make_tuple_range(Tuple&& t) noexcept
{
    static_assert(FromIndex <= ToIndex,
                  "FromIndex must be less than or equal to ToIndex");

    return detail::make_tuple_range_impl<FromIndex>(
            std::make_index_sequence<ToIndex - FromIndex>(),
            std::forward<Tuple>(t));
}

// make_tuple_view
template<std::size_t... Is, typename Tuple>
constexpr auto make_tuple_view(Tuple&& t) noexcept
{
    return std::forward_as_tuple(std::get<Is>(std::forward<Tuple>(t))...);
}

Tests:

double pi = 3.14;
std::tuple<int, double&, const char, float> t(42, pi, 'c', 0);

// non-const lvalue
static_assert(std::is_same<
                  decltype(make_tuple_range<0, 3>(t)),
                  std::tuple<int&, double&, const char&>
              >::value, "");

// const lvalue
const auto& ct = t;
static_assert(std::is_same<
                  decltype(make_tuple_view<3, 0, 2, 1>(ct)),
                  std::tuple<const float&, const int&, const char&, double&>
              >::value, "");

// non-const rvalue
static_assert(std::is_same<
                  decltype(make_tuple_range<1, 4>(std::move(t))),
                  std::tuple<double&, const char&&, float&&>
              >::value, "");

// const rvalue
const auto&& crt = std::move(t);
static_assert(std::is_same<
                  decltype(make_tuple_range<1, 4>(std::move(crt))),
                  std::tuple<double&, const char&, const float&>
              >::value, "");

However, I'm not entirely confident in my implementation. Is there any edge case where incorrect reference types can be deduced?

UPDATE: Thanks for all the awesome answers, TIL for sure! However, I'm still wondering if my definition of make_tuple_view/range can deduce incorrect return types. The purpose of this question is not so much to look for the optimal implementation as to learn about the details of this part of the type system. I'd really appreciate it if more focus can be put on the deduced return type for my make_tuple_view/range functions.

like image 482
Zizheng Tai Avatar asked Jan 05 '23 17:01

Zizheng Tai


2 Answers

namespace details {
  template<std::size_t...Is,class TupleIn>
  auto tuple_view( std::index_sequence<Is...>, TupleIn&& tin) {
    return std::forward_as_tuple( std::get<Is>(std::forward<TupleIn>(tin))... );
  }
}
template<class TupleIn>
auto tuple_view(TupleIn&& tin) {
  auto indexes = std::make_index_sequence< std::tuple_size<std::remove_reference_t<TupleIn>>{} >;
  return details::tuple_view( indexes, std::forward<TupleIn>(tin) );
}

and done?

To move more code to boilerplate:

template<std::size_t I>
using index_t = std::integral_constant< std::size_t, I >;
template<std::size_t I>
constexpr index_t<I> index{};

template<std::size_t...Is>
auto unpack_indexes( std::index_sequence<Is...> ) {
  return [](auto&& f) {
    return decltype(f)(f)( index<Is>... );
  };
}
template<std::size_t N>
auto unpack_indexes( index_t<N> ) {
  return unpack_indexes( std::make_index_sequence<N>{} );
};

template<class TupleIn>
auto tuple_view(TupleIn&& tin) {
  auto size = std::tuple_size<std::remove_reference_t<TupleIn>>{};
  auto indexes = unpack_indexes(size);
  return indexes(
    [&](auto...Is) {
      return std::forward_as_tuple( std::get<Is>(std::forward<TupleIn>(tin))... );
    }
  );
}

which does a bunch of (reusable) work to get rid of details namespace.

Writing code to manipulate the indexes should be done orthogonally to making the view.

like image 167
Yakk - Adam Nevraumont Avatar answered Jan 15 '23 07:01

Yakk - Adam Nevraumont


Way too complicated. std::tie almost does all the work for you.

#include <tuple>
#include <iostream>


template<class Tuple, std::size_t...Is>
auto tuple_view(Tuple&& t, std::index_sequence<Is...>)
{
    return std::tie(std::get<Is>(std::forward<Tuple>(t))...);
}

int main()
{
    auto a = std::make_tuple(1,2,3);

    auto b = tuple_view(a, std::index_sequence<0,1,2,1,0>());

    std::cout << std::get<0>(b) << std::endl;
    std::cout << std::get<1>(b) << std::endl;
    std::cout << std::get<2>(b) << std::endl;
    std::cout << std::get<3>(b) << std::endl;
    std::cout << std::get<4>(b) << std::endl;
}

expected output:

1
2
3
2
1
like image 28
Richard Hodges Avatar answered Jan 15 '23 08:01

Richard Hodges