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))...);
}
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:
Is there any edge case that can cause incorrect reference types to be deduced?
Can the implementation be simplified (without changing the interface and introducing any runtime cost)?
1 Answer 1
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.)