Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
46 changes: 46 additions & 0 deletions examples/example_next_to_range.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#include <dice/template-library/next_to_range.hpp>

#include <cstddef>
#include <iostream>
#include <iterator>
#include <optional>
#include <ranges>

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<int> next() {
return cur_++;
}
};

// Create just a C++-style iterator
using iota_iter = dtl::next_to_iter<iota_iter_impl>;
static_assert(std::input_iterator<iota_iter>);

// Create a C++-style range
using iota = dtl::next_to_range<iota_iter>;
static_assert(std::ranges::range<iota>);


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';
}
}
175 changes: 175 additions & 0 deletions include/dice/template-library/next_to_range.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
#ifndef DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP
#define DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP

#include <cassert>
#include <cstddef>
#include <functional>
#include <iterator>
#include <optional>
#include <type_traits>
#include <utility>

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<std::optional<typename Iter::value_type>>;
*
* We are not using a concept here, because concepts require things to be public
*/
template<typename Iter>
Comment thread
bigerl marked this conversation as resolved.
Comment thread
bigerl marked this conversation as resolved.
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<value_type> cur_;
std::optional<value_type> 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<typename ...Args>
explicit next_to_iter(Args &&...args) : Iter{std::forward<Args>(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.
Comment thread
bigerl marked this conversation as resolved.
* 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<std::is_copy_constructible_v<base_iterator>, next_to_iter, void> operator++(int) {
if constexpr (std::is_copy_constructible_v<base_iterator>) {
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<value_type> 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<std::optional<typename Iter::value_type>>;
*
* We are not using a concept here, because concepts require things to be public
*/
template<typename Iter>
struct next_to_range {
using iterator = next_to_iter<Iter>;
using sentinel = typename iterator::sentinel;
using value_type = typename iterator::value_type;

private:
std::conditional_t<
std::is_copy_constructible_v<Iter>,
Iter,
std::function<iterator()>
> make_iter_;

public:
template<typename ...Args> requires (!std::is_copy_constructible_v<Iter>)
explicit next_to_range(Args &&...args)
: make_iter_{[...args = std::forward<Args>(args)] { return iterator{args...}; }} {
}

template<typename ...Args> requires (std::is_copy_constructible_v<Iter>)
explicit next_to_range(Args &&...args)
: make_iter_{std::forward<Args>(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<Iter>) {
return iterator{make_iter_};
} else {
return make_iter_();
}
}

static constexpr sentinel end() noexcept {
return sentinel{};
}
};

} // namespace dice::template_library


#endif // DICE_TEMPLATELIBRARY_NEXTTORANGE_HPP
3 changes: 3 additions & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
123 changes: 123 additions & 0 deletions tests/tests_next_to_range.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <doctest/doctest.h>

#include <dice/template-library/next_to_range.hpp>


#include <algorithm>
#include <cstddef>
#include <initializer_list>
#include <optional>
#include <ranges>
#include <vector>

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<int> next() {
return cur_++;
}
};

using non_copy_iota = dtl::next_to_range<non_copy_iota_iter>;
static_assert(std::ranges::range<non_copy_iota>);

struct values_yielder_iter {
using value_type = int;

private:
std::vector<int> values_;
size_t ix_ = 0;

public:
explicit values_yielder_iter(std::initializer_list<int> values) : values_{values} {
}

protected:
[[nodiscard]] std::optional<int> next() {
if (ix_ >= values_.size()) {
return std::nullopt;
}

return values_[ix_++];
}
};

using values_yielder = dtl::next_to_range<values_yielder_iter>;
static_assert(std::ranges::range<values_yielder>);


TEST_CASE("sanity check") {
non_copy_iota ints{0};
CHECK(std::ranges::equal(ints | std::views::take(3), std::vector<int>{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<int>{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<decltype(iter++), void>);

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<decltype(iter++), typename values_yielder::iterator>);

CHECK_EQ(*iter, 0);

auto cpy = iter++;
CHECK_EQ(*cpy, 0);
CHECK_EQ(*iter, 1);
}
}
}
Loading