6
\$\begingroup\$

This interesting idea came up when I was designing the for_each for tuple-like objects in this post. The for_each in that post makes it possible to write code like:

auto t = std::make_tuple(42, 'c', 3.14);
for_each(t, [](auto x) { std::cout << x << '\n'; });

But unfortunately, this way we can only iterate over the entire tuple-like object, unlike std::for_each, which, by accepting iterators, enables us to iterate over only a part of a collection. If only we could achieve something similar for tuple-likes!

I guess theoretically it is possible to create some kind of heterogeneous iterator, but that feels kind of weird, because iterator types usually inherit from std::iterator<T, ...> for some fixed T.

So I think for tuple-like objects, a good way is to create some kind of ranges or views into the original objects, specified by indices. A range is a tuple of appropriate types of references to consecutive elements in the original tuple and preserves element order. A view is a tuple of appropriate types of references to possibly non-consecutive elements in the original tuple and does not necessarily preserve element order. Examples (pseudo-code):

(lvalue) std::tuple<int, const char, double>:
    range<0, 0>  : std::tuple<>
    range<0, 1>  : std::tuple<int&>
    range<0, 2>  : std::tuple<int&, const char&>
    view<2, 0>   : std::tuple<double&, int&>
    view<1, 2, 0>: std::tuple<const char&, double&, int&>

(rvalue) std::tuple<int, char, double>:
    range<0, 2>: std::tuple<int&&, char&&>
    view<2>    : std::tuple<double&&>

(lvalue) std::tuple<int&, int>:
    range<0, 2>: std::tuple<int&, int&>

...

Then, we will be able to do something like:

auto t = std::make_tuple(42, 'c', 3.14);

for_each(make_tuple_range<1, 3>(t), [](auto x) { std::cout << x << ' '; });
// prints: c 3.14

for_each(make_tuple_view<2, 0, 1>(t), [](auto x) { std::cout << x << ' '; });
// prints: 3.14 42 c

for_each(make_tuple_range<1, 3>(t), [](auto& x) { x += 1; });
for_each(t, [](auto x) { std::cout << x << ' '; });
// prints: 42 d 4.14

Enough examples. Time for implementation (C++14):

#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, "");

Main concerns:

  1. Is there any edge case that can cause incorrect reference types to be deduced?

  2. Can the implementation be simplified (without changing the interface and introducing any runtime cost)?

\$\endgroup\$
2
\$\begingroup\$

My 2 cents:

I think you should restructure your code so that a range is simply a contiguous index_sequence; and taking a subtuple by range is simply taking those elements corresponding to the index sequence. So you would implement a make_index_range which is similar to std::make_index_sequence, and then using that range you would have something like

template <class F, Tuple t, size_t... Is>
constexpr auto subtuple(F f, Tuple t, std::index_sequence<Is...>) {
    return std::make_tuple(std::get<std::integral_constant<size_t, Is>>(t)...);
}

(there may need to be forwarding in there, I'm not sure.)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.