2
\$\begingroup\$

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;
}


\$\endgroup\$
3

1 Answer 1

3
\$\begingroup\$

Missing iterators

As it is now, I would say it is cute, but not very useful. In most of your test code, you can see that using std::get<>() requires less typing than using util::elements<>(). However, your class would really shine if it implemented iterators, such that you can do:

for (auto& e0: util::elements<0>(vtop)) {
    std::cout << e0.first << '\n';
}

But also so that you can use many of the STL algorithms on your view.

There is no need for the helper templates

You can make your code deduce the right type for data_ without needing lref_iff_lref_t. In:

template<size_t I, class U>
auto elements(U&& u) {
    return ElementsView<I, decltype(u)>{std::forward<U>(u)};
}

If u is an lvalue-reference, U is deduced as U&. However, if u is an rvalue-reference, U is deduced as U. However, decltype(u) will deduce the rvalue-reference that you don't want. The solution is to simply remove the decltype() here:

return ElementsView<I, U>{std::forward<U>(u)};

There will be some compile errors then due to the fwd_* templates deducing the wrong thing. However, a much simpler solution is to let the compiler deduce the type for you from the std::get<>() expression you are using:

using value_type = decltype(std::get<index>(data_[0]));

However, both your original value_type and the one using decltype() have an issue:

What about const views?

There is a problem when making a const view.The const-qualified operator[] will still return a mutable reference to data_, if the latter is an lvalue reference to a non-const container. Slapping a const in front of it doesn't work though, because that will make it a const reference instead of a reference to const data. You have to remove the reference from value_type first, then add the const & back to it:

const std::remove_reference_t<value_type>& operator[](size_t pos) const {
    return std::get<index>(data_[pos]);
}

Or better, don't let value_type lie, and make sure it is a value type and not a reference to begin with:

using value_type = std::remove_reference_t<...>;

Note that you could also have avoided the issue by using auto return type deduction:

const auto& operator[](size_t pos) const {
    return std::get<index>(data_[pos]);
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ Fantastic. You shaved off 70% of the cruft and found a const bug with an elegant solution. Most importantly I understood everything. As to iterators, maybe. I'm using this with a "zip" view wrapper which provides them (and where std algorithms work ofc) Maybe I'll post that soon, and looking forward to your 70% cut there. ;-) \$\endgroup\$
    – joao
    May 17, 2022 at 21:05

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.