Making a custom C++ ranges view

By int2str πŸ‡ΊπŸ‡¦ (@int2str.net)
Published:

When starting to experiment more with C++20 / C++23's std::ranges and std::views, the question inevitably comes up as to what it takes to make a custom view. While there are articles and examples out there, they range from overly simplistic (just an alias/using) to overly complicated. Further, there are many examples, even in popular talks, that simply don't compile.

There are three main ways in which a custom view can interact with ranges/views which we will explain in more detail below.

Full (compiling ;) ) source code for this example can be found here, if you want to read along: https://github.com/int2str/nmpairs

Bubbles!

To keep the example as simple as possible, we will demonstrate a custom nm_pairs_view. This is a specialized version of k-combinations, where k is 2.

In otherwords, the "bubble sort" iterator.

Here's the classic bubble sort algorithm in C++: ``C++ for (size_t n = 0; n != values.size() - 1; ++n) { for (size_t m = n + 1; m != values.size(); ++m) { if (values[m] < values[n]) std::swap(values[n], values[m]); } } CODEBLOCK0C++ auto less = [](const auto& pair) { return pair.second < pair.first; }; auto swap = [](auto pair) { std::swap(pair.first, pair.second); }; std::ranges::for_each( range | views::nm_pairs | std::views::filter(less), swap); CODEBLOCK1C++ class hello_view : public std::ranges::view_interface<hello_view> { static constexpr auto hello = std::string_view("Hello, World!\n"); public: auto begin() { return hello.begin(); }; auto end() { return hello.end(); } }; auto main() -> int { for (const auto& chr : hello_view{} | std::views::drop(7)) std::print("{}", chr); } CODEBLOCK2C++ template <std::ranges::forward_range RANGE> requires std::ranges::view<RANGE> class nm_pairs_view : public std::ranges::view_interface<nm_pairs_view<RANGE>> { RANGE base_; public: [[nodiscard]] constexpr nm_pairs_view() = default; [[nodiscard]] constexpr explicit nm_pairs_view(RANGE range) : base_{std::move(range)} {} ... }; CODEBLOCK3C++ template <std::ranges::sized_range RANGE> nm_pairs_view(RANGE&&) -> nm_pairs_view<std::views::all_t<RANGE>>; CODEBLOCK4C++ std::vector<int> numbers{1, 2, 3, 4}; for (const auto& [a, b] : nm_pairs_view(numbers)) std::print("[{}, {}] ", a, b); // Prints: [1, 2] [1, 3] [1, 4] [2, 3] [2, 4] [3, 4] CODEBLOCK5 C(R) R | C CODEBLOCK6C++ namespace views { struct nm_pairs_fn : std::ranges::range_adaptor_closure<nm_pairs_fn> { template <typename RANGE> constexpr auto operator()(RANGE&& range) const { return nm_pairs_view{std::forward<RANGE>(range)}; } }; constexpr inline auto nm_pairs = nm_pairs_fn{}; } // namespace views CODEBLOCK7C++ for (const auto& [a, b] : numbers | views::nm_pairs_fn()) ... CODEBLOCK8C++ for (const auto& [a, b] : numbers | views::nm_pairs) ... CODEBLOCK9C++ auto my_vector = std::views::iota(1) | std::views::take(5) | std::ranges::to<std::vector>(); CODEBLOCK10C++ template <typename RANGE> constexpr CoordinateSet(std::from_range_t /*unused*/, RANGE&& range) { for (auto coordinate : range) insert(coordinate); } CODEBLOCK11C++ auto coordinate_set = coordinates | std::views::filter(in_bounds) | std::ranges::to<CoordinateSet>(); ``