diff --git a/README.md b/README.md index 849b883..e010089 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ It contains: - `mutex`/`shared_mutex`: Rust inspired mutex interfaces that hold their data instead of living next to it - `static_string`: A string type that is smaller than `std::string` for use cases where you do not need to resize the string - `ranges`: Additional range algorithms and adaptors that are missing from the standard library. +- `next_to_range`/`next_to_iter`: Eliminate the boilerplate required to write C++ iterators and ranges. ## Usage @@ -136,6 +137,10 @@ It also supports allocators with "fancy" pointers. Additional range algorithms (e.g. `unique_view`) and adaptors (e.g., a pipeable `all_of`) that are missing from the standard library. +### `next_to_range`/`next_to_iter` +Eliminate the boilerplate required to write C++ iterators and ranges. +To get a fully functional range, the only thing that is required is +implementing a minimal, rust-style iterator interface. ### Further Examples diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 352f3f9..fd38d5d 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -136,3 +136,10 @@ target_link_libraries(example_static_string dice-template-library::dice-template-library ) + +add_executable(example_next_to_range + example_next_to_range.cpp) +target_link_libraries(example_next_to_range + PRIVATE + dice-template-library::dice-template-library +) diff --git a/examples/example_next_to_range.cpp b/examples/example_next_to_range.cpp new file mode 100644 index 0000000..2f5d0b3 --- /dev/null +++ b/examples/example_next_to_range.cpp @@ -0,0 +1,46 @@ +#include + +#include +#include +#include +#include +#include + +namespace dtl = dice::template_library; + +struct iota_iter_impl { + using value_type = int; + +private: + int cur_; + +public: + explicit iota_iter_impl(int start = 0) noexcept : cur_{start} { + } + +protected: + [[nodiscard]] std::optional next() { + return cur_++; + } +}; + +// Create just a C++-style iterator +using iota_iter = dtl::next_to_iter; +static_assert(std::input_iterator); + +// Create a C++-style range +using iota = dtl::next_to_range; +static_assert(std::ranges::range); + + +int main() { + size_t num_iter = 0; + for (auto it = iota_iter{}; num_iter < 10; ++it) { + std::cout << *it << '\n'; + ++num_iter; + } + + for (int const val : iota{5} | std::views::take(10)) { + std::cout << val << '\n'; + } +} diff --git a/include/dice/template-library/next_to_range.hpp b/include/dice/template-library/next_to_range.hpp new file mode 100644 index 0000000..e9338bc --- /dev/null +++ b/include/dice/template-library/next_to_range.hpp @@ -0,0 +1,175 @@ +#ifndef DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP +#define DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP + +#include +#include +#include +#include +#include +#include +#include + +namespace dice::template_library { + + /** + * Wrapper to make a C++-style iterator out of a rust-style iterator. + * + * @tparam Iter base rust-style iterator + * + * Requirements for Iter (the following things must be at least protected): + * typename Iter::value_type; + * { Iter::next() } -> std::same_as>; + * + * We are not using a concept here, because concepts require things to be public + */ + template + struct next_to_iter : Iter { + using base_iterator = Iter; + using sentinel = std::default_sentinel_t; + using value_type = typename Iter::value_type; + using reference = value_type const &; + using pointer = value_type const *; + using difference_type = std::ptrdiff_t; + using iterator_category = std::input_iterator_tag; + + private: + std::optional cur_; + std::optional peeked_; + + void advance() { + if (peeked_.has_value()) [[unlikely]] { + // fast path, we have already peaked the value + cur_ = std::exchange(peeked_, std::nullopt); + } else { + cur_ = this->next(); + } + } + + public: + template + explicit next_to_iter(Args &&...args) : Iter{std::forward(args)...}, + cur_{this->next()} { + } + + [[nodiscard]] reference operator*() const noexcept { + assert(cur_.has_value()); + return *cur_; + } + + [[nodiscard]] pointer operator->() const noexcept { + assert(cur_.has_value()); + return &*cur_; + } + + /** + * Access the current value of the iterator mutably. + * This is required because in ranges, iterators must have the same reference type for const and non-const access of operator*. + * This means a non-const overload of operator* that returns a non-const reference is not allowed. + * + * @return reference to current value + */ + [[nodiscard]] value_type &value() noexcept { + assert(cur_.has_value()); + return *cur_; + } + + [[nodiscard]] value_type const &value() const noexcept { + assert(cur_.has_value()); + return *cur_; + } + + next_to_iter &operator++() { + advance(); + return *this; + } + + std::conditional_t, next_to_iter, void> operator++(int) { + if constexpr (std::is_copy_constructible_v) { + auto cpy = *this; + advance(); + return cpy; + } else { + advance(); + } + } + + /** + * Takes a peek at the next element if there is one, but does not advance the iterator onto it. + * This function is meant to be used when the underlying iterator is not copyable or expensive to copy. + * + * @return nullopt if there is no next element, the element if there is a next element + */ + [[nodiscard]] std::optional const &peek() { + if (!peeked_.has_value()) { + peeked_ = this->next(); + } + + return peeked_; + } + + friend bool operator==(next_to_iter const &self, sentinel) noexcept { + return !self.cur_.has_value(); + } + + friend bool operator==(sentinel, next_to_iter const &self) noexcept { + return !self.cur_.has_value(); + } + }; + + /** + * Make a C++-style range out of a rust-style iterator. + * This is meant to save you from writing all the boilerplate that is required for C++ ranges and iterators. + * + * @tparam Iter base rust-style iterator + * + * Requirements for Iter (the following things must be at least protected): + * typename Iter::value_type; + * { Iter::next() } -> std::same_as>; + * + * We are not using a concept here, because concepts require things to be public + */ + template + struct next_to_range { + using iterator = next_to_iter; + using sentinel = typename iterator::sentinel; + using value_type = typename iterator::value_type; + + private: + std::conditional_t< + std::is_copy_constructible_v, + Iter, + std::function + > make_iter_; + + public: + template requires (!std::is_copy_constructible_v) + explicit next_to_range(Args &&...args) + : make_iter_{[...args = std::forward(args)] { return iterator{args...}; }} { + } + + template requires (std::is_copy_constructible_v) + explicit next_to_range(Args &&...args) + : make_iter_{std::forward(args)...} { + } + + /** + * @return a new iterator from the beginning of the range + * @note this *always* returns a new iterator, regardless if there are other iterators alive + */ + [[nodiscard]] iterator begin() const { + if constexpr (std::is_copy_constructible_v) { + return iterator{make_iter_}; + } else { + return make_iter_(); + } + } + + static constexpr sentinel end() noexcept { + return sentinel{}; + } + }; + +} // namespace dice::template_library + + +#endif // DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e3b0b41..8f292eb 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -87,3 +87,6 @@ custom_add_test(tests_shared_mutex) add_executable(tests_static_string tests_static_string.cpp) custom_add_test(tests_static_string) + +add_executable(tests_next_to_range tests_next_to_range.cpp) +custom_add_test(tests_next_to_range) diff --git a/tests/tests_next_to_range.cpp b/tests/tests_next_to_range.cpp new file mode 100644 index 0000000..15cbea0 --- /dev/null +++ b/tests/tests_next_to_range.cpp @@ -0,0 +1,123 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include + + +#include +#include +#include +#include +#include +#include + +namespace dtl = dice::template_library; + +TEST_SUITE("next_to_range") { + struct non_copy_iota_iter { + using value_type = int; + + private: + int cur_; + + public: + explicit non_copy_iota_iter(int start = 0) noexcept : cur_{start} { + } + + non_copy_iota_iter(non_copy_iota_iter &&other) = default; + non_copy_iota_iter &operator=(non_copy_iota_iter &&other) = default; + non_copy_iota_iter(non_copy_iota_iter const &other) = delete; + non_copy_iota_iter &operator=(non_copy_iota_iter const &other) = delete; + ~non_copy_iota_iter() = default; + + protected: + [[nodiscard]] std::optional next() { + return cur_++; + } + }; + + using non_copy_iota = dtl::next_to_range; + static_assert(std::ranges::range); + + struct values_yielder_iter { + using value_type = int; + + private: + std::vector values_; + size_t ix_ = 0; + + public: + explicit values_yielder_iter(std::initializer_list values) : values_{values} { + } + + protected: + [[nodiscard]] std::optional next() { + if (ix_ >= values_.size()) { + return std::nullopt; + } + + return values_[ix_++]; + } + }; + + using values_yielder = dtl::next_to_range; + static_assert(std::ranges::range); + + + TEST_CASE("sanity check") { + non_copy_iota ints{0}; + CHECK(std::ranges::equal(ints | std::views::take(3), std::vector{0, 1, 2})); + + auto even_ints = ints + | std::views::transform([](int x) { return x + 1; }) + | std::views::filter([](int x) { return x % 2 == 0; }) + | std::views::take(3); + + CHECK(std::ranges::equal(even_ints, std::vector{2, 4, 6})); + } + + TEST_CASE("peeking") { + values_yielder ints{1, 2}; + + auto iter = ints.begin(); + CHECK_NE(iter, ints.end()); + CHECK_EQ(*iter, 1); + CHECK_EQ(iter.peek(), 2); + + ++iter; + + CHECK_NE(iter, ints.end()); + CHECK_EQ(*iter, 2); + CHECK_EQ(iter.peek(), std::nullopt); + + ++iter; + CHECK_EQ(iter, ints.end()); + CHECK_EQ(iter.peek(), std::nullopt); + } + + TEST_CASE("postincrement") { + SUBCASE("non-copyable") { + non_copy_iota const ints{0}; + auto iter = ints.begin(); + + static_assert(std::same_as); + + CHECK_EQ(*iter, 0); + iter++; + CHECK_EQ(*iter, 1); + } + + SUBCASE("copyable") { + values_yielder const ints{0, 1}; + auto iter = ints.begin(); + + static_assert(std::same_as); + + CHECK_EQ(*iter, 0); + + auto cpy = iter++; + CHECK_EQ(*cpy, 0); + CHECK_EQ(*iter, 1); + } + } +}