For mainly didactic reasons, I have designed my ElementsView<I, T>
class template, which provides a view over the I
th elements of each tuple contained by a given container T
, provided that container supports operator[]
. The entry point is the factory function template animal::util::elements<I>(...sequence-container-of-tuples...)
.
This is similar (though way less powerful) to C++20's std::ranges::elements_view
, and is designed to interop with a custom "zip" view (not shown, maybe in some other review).
I'm looking for any kind of insight/criticism, but my main doubt is about this lref_iff_lref_t
helper I "invented" to the elements
functions accept rvalues. That is, so I can write things like:
elements<1>(elements<2>(container_of_tuples_of_tuples));
I'm also looking for simplification, hidden performance problems and UB.
So here is the code, follow by "Catch2" tests
// eview.hpp
#include <tuple>
#ifdef EVIEW_MP_TESTS
#include <type_traits>
#include <vector>
#endif
namespace animal::util::eview {
using std::size_t, std::remove_reference_t, std::decay_t,
std::tuple_element_t, std::is_same_v;
/**
* fwd_tup_elem_t<I, TUP>
*
* Get Ith type of tuple TUP, cvref'ed as TUP */
template <size_t I, typename Tup>
struct fwd_tup_elem;
template <size_t I, typename Tup>
using fwd_tup_elem_t = typename fwd_tup_elem<I, Tup>::type;
template <size_t I, typename T>
struct fwd_tup_elem<I, const T> {using type = const fwd_tup_elem_t<I, T>;};
template <size_t I, typename T>
struct fwd_tup_elem<I, T&> {
using type = tuple_element_t<I, remove_reference_t<T>> &;
};
template <size_t I, typename T>
struct fwd_tup_elem<I, T&&> {
using type = tuple_element_t<I, remove_reference_t<T>> &;
};
/**
* fwd_cont_val<TC>
*
* Get value_type of tuple container TC, cvref'ed as TC */
template <size_t I, typename TC>
struct fwd_cont_val;
template <size_t I, typename TC>
using fwd_cont_val_t = typename fwd_cont_val<I, TC>::type;
template <size_t I, typename TC>
struct fwd_cont_val<I, const TC&> {
using type = const fwd_tup_elem_t<I, const typename TC::value_type&>;
};
template <size_t I, typename TC>
struct fwd_cont_val<I, TC&> {
using type = fwd_tup_elem_t<I, typename TC::value_type&>;
};
template <size_t I, typename TC>
struct fwd_cont_val<I, TC&&> {
using type = fwd_tup_elem_t<I, typename TC::value_type&>;
};
#ifdef EVIEW_MP_TESTS
// ERROR, GOOD: non-supported tuple type
// static_assert(is_same_v<tup_fwd_t<0, std::tuple<int>>, int>);
static_assert(is_same_v<tup_fwd_t<0, std::tuple<int>&>, int&>);
static_assert(is_same_v<tup_fwd_t<0, const std::tuple<int>&>, const int&>);
static_assert(is_same_v<tup_fwd_t<0, const std::tuple<int>&&>, const int&>);
// ERROR, GOOD: non-supported vector type
// static_assert(is_same_v<val_fwd_t<0, std::vector<std::tuple<int>>>, int>);
static_assert(is_same_v<val_fwd_t<0, std::vector<std::tuple<int>>&>, int&>);
static_assert(is_same_v<val_fwd_t<0, std::vector<std::tuple<int>>&&>, int&>);
static_assert(is_same_v<val_fwd_t<0, const std::vector<std::tuple<int>>&>, const int&>);
#endif
/**
* lref_iff_lref_t<TC>
*
* TC& if TC is T&. TC if TC is T&&. Error otherwise */
template <typename T>
struct lref_iff_lref;
template <typename T>
using lref_iff_lref_t = typename lref_iff_lref<T>::type;
template <typename T>
struct lref_iff_lref<T&> {using type = T&;};
template <typename T>
struct lref_iff_lref<T&&> {using type = remove_reference_t<T>;};
/**
* ElementsView<I, TC>
*
* View over Ith elements of each tuple contained by container TC.
*
* C++17. Only operator[] and size() supported. Kind of like
* std::ranges::elements_view (C++20), but works with custom "zip"
* pseudo-container (not shown) as std::ranges::zip_view not available
* until C++23. */
template <size_t I, typename TC>
class ElementsView {
static constexpr size_t index = I;
public:
using value_type = fwd_cont_val_t<I, TC>;
explicit ElementsView(TC c) : data_{c} {}
value_type& operator[](size_t pos) { return std::get<index>(data_[pos]);}
value_type& operator[](size_t pos) const { return std::get<index>(data_[pos]);}
auto size() const { return data_.size();}
private:
lref_iff_lref_t<TC> data_;
};
template<size_t I, class U>
auto elements(U&& u) { return ElementsView<I, decltype(u)>{std::forward<U>(u)};}
} // namespace animal::util::eview
namespace animal::util { using eview::elements; }
// eview-tests.hpp
#include "catch2/catch_test_macros.hpp"
#include <array>
namespace util = animal::util;
TEST_CASE("basic util::elements") {
std::array<std::pair<int, char>, 3> ap{{{1, 'a'}, {2, 'b'}, {3, 'c'}}};
auto ev1 = util::elements<1>(ap);
ev1[1] = 'z';
auto ev0 = util::elements<0>(ap);
ev0[1] = 42;
const auto& cap = ap;
REQUIRE(cap[1].first == 42);
REQUIRE(cap[1].second == 'z');
auto cev0 = util::elements<0>(cap);
// TD<decltype(cev0)::value_type> mistery;
// ev3[2] = 26; // ERROR, GOOD: cap is const, so ev3 has const value_type
auto cev1 = util::elements<1>(cap);
REQUIRE(cev0[1] == 42);
REQUIRE(cev1[1] == 'z');
}
TEST_CASE("more util::elements") {
using TOP = std::tuple<std::pair<int, std::pair<int, int>>, char, std::string>;
std::vector<TOP> vtop { {{1, {11, 111}}, 'A', "a"}, {{2, {22, 222}}, 'B', "b"} };
auto ev0 = util::elements<0>(vtop);
REQUIRE(ev0[1].first == 2);
util::elements<0>(vtop)[1].first = 3;
REQUIRE(util::elements<0>(vtop)[1].first == 3);
REQUIRE(std::get<0>(vtop[1]).first == 3);
auto composed{util::elements<0>(util::elements<0>(vtop))};
REQUIRE(composed[1] == 3);
composed[1] = 4;
REQUIRE(util::elements<0>(vtop)[1].first == 4);
REQUIRE(std::get<0>(vtop[1]).first == 4);
const auto& cvtop = vtop;
auto intermediate = util::elements<0>(cvtop);
auto composed2{util::elements<0>(util::elements<0>(cvtop))};
REQUIRE(composed2[1] == 4);
// composed2[1] = 777; // ERROR, GOOD: source owning container is const
auto composed3{util::elements<1>(util::elements<1>(util::elements<0>(cvtop)))};
REQUIRE(composed3[1] == 222);
// composed3[1] = 777;
}