Skip to content

Commit

Permalink
2023: add row iterator for Grid
Browse files Browse the repository at this point in the history
For the old behaviour (iterating over the flat underlying array), use
`Grid::data()` instead.
  • Loading branch information
yut23 committed Jan 24, 2024
1 parent f2d6f6b commit edc3060
Show file tree
Hide file tree
Showing 4 changed files with 280 additions and 72 deletions.
311 changes: 257 additions & 54 deletions 2023/src/data_structures.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,29 @@
#ifndef DATA_STRUCTURES_HPP_KBRH53YC
#define DATA_STRUCTURES_HPP_KBRH53YC

#include "lib.hpp" // for DEBUG
#include "util/concepts.hpp" // for const_or_rvalue_ref
#include "lib.hpp" // for Pos, DEBUG
#include "util/concepts.hpp" // for any_convertible_range

#include <algorithm> // for copy, move
#include <algorithm> // for copy
#include <cassert> // for assert
#include <cstddef> // for size_t
#include <compare> // for partial_ordering
#include <concepts> // for predicate, same_as, convertible_to
#include <cstddef> // for size_t, ptrdiff_t
#include <functional> // for less, greater
#include <initializer_list> // for initializer_list
#include <iterator> // for next, advance
#include <iterator> // for next, advance, back_inserter, misc concepts
#include <list> // for list
#include <memory> // for shared_ptr, weak_ptr, make_shared, enable_shared_from_this
#include <string> // for string
#include <type_traits> // for is_invocable_r_v // IWYU pragma: keep
#include <utility> // for move, swap, forward, pair
#include <vector> // for vector
#include <ranges> // for range, range_value_t
#include <span> // for span, dynamic_extent // IWYU pragma: export
#include <string> // for string, basic_string
#include <type_traits> // for is_same_v, conditional_t // IWYU pragma: export
#include <utility> // for move, swap, forward, pair, make_pair
#include <vector> // for vector, __cpp_lib_constexpr_vector

namespace aoc::ds {

template <class T, class Compare = std::less<T>>
requires std::is_invocable_r_v<bool, Compare, T, T>
template <class T, std::predicate<T, T> Compare = std::less<T>>
class pairing_heap {
public:
using value_compare = Compare;
Expand Down Expand Up @@ -302,6 +305,151 @@ pairing_heap(InputIt, InputIt, const Compare & = Compare())
-> pairing_heap<typename std::iterator_traits<InputIt>::value_type,
Compare>;

/*
* This is strictly a bidirectional iterator, although it supports all
* operations needed for a random-access iterator aside from subscripting. It
* is also an input iterator, since we don't want to allow writing to an entire
* row.
*
* Based on
* https://www.internalpointers.com/post/writing-custom-iterators-modern-cpp
*/
template <class T>
struct RowIterator {
using iterator_category = std::bidirectional_iterator_tag;
using difference_type = std::ptrdiff_t;
using value_type = std::span<T, std::dynamic_extent>;
using pointer = const std::span<T> *;
using reference = const std::span<T> &;

RowIterator() = default;

template <class R>
explicit RowIterator(R &&range, std::size_t width)
: m_span(range), m_width(width), m_index(0),
m_curr_span(m_span.subspan(m_index * m_width, m_width)) {}

// this is intentionally implicit to allow converting a non-const iterator
// into a const one
template <class U>
requires std::same_as<T, const U>
// cppcheck-suppress noExplicitConstructor
RowIterator(const RowIterator<U> &other)
: m_span(other.m_span), m_width(other.m_width), m_index(other.m_index),
m_curr_span(other.m_curr_span) {}

reference operator*() const { return m_curr_span; }
pointer operator->() const { return &m_curr_span; }
/*
* This is all that's needed to be a full random-access iterator, but we
* would have to store the subspan in order to return a reference.
*/
// reference operator[](difference_type off) const {
// return m_span.subspan((m_index + off) * m_width, m_width);
// }

// Prefix increment
RowIterator &operator++() {
++m_index;
update_curr_span();
return *this;
}
// Postfix increment
RowIterator operator++(int) {
RowIterator tmp = *this;
++(*this);
return tmp;
}

// Prefix decrement
RowIterator &operator--() {
--m_index;
update_curr_span();
return *this;
}
// Postfix decrement
RowIterator operator--(int) {
RowIterator tmp = *this;
--(*this);
return tmp;
}

// random-access iterator operations
RowIterator &operator+=(difference_type n) {
m_index += n;
if (n != 0) {
update_curr_span();
}
return *this;
}
RowIterator operator+(difference_type rhs) const {
RowIterator tmp = *this;
tmp += rhs;
return tmp;
}
// pass-by-value makes a copy in rhs
friend RowIterator operator+(difference_type lhs, RowIterator rhs) {
rhs += lhs;
return rhs;
}

RowIterator &operator-=(difference_type n) {
m_index += -n;
return *this;
}
RowIterator operator-(difference_type rhs) const {
RowIterator tmp = *this;
tmp += -rhs;
return tmp;
}

difference_type operator-(const RowIterator &rhs) const {
assert(this->m_span.data() == rhs.m_span.data());
return this->m_index - rhs.m_index;
}

friend std::partial_ordering operator<=>(const RowIterator &lhs,
const RowIterator &rhs) {
if (lhs.m_curr_span.data() == rhs.m_curr_span.data()) {
return std::partial_ordering::equivalent;
}
if (lhs.m_span.data() != rhs.m_span.data()) {
// lhs and rhs belong to different Grids
return std::partial_ordering::unordered;
}
return lhs.m_index <=> rhs.m_index;
}

// RowIterators that have been advanced past the end have an empty
// m_curr_span (data() returns nullptr), which is also true of
// default-constructed RowIterators, so RowIterator() is a valid sentinel.
friend bool operator==(const RowIterator &a, const RowIterator &b) {
return a.m_curr_span.data() == b.m_curr_span.data();
}
friend bool operator!=(const RowIterator &a, const RowIterator &b) {
return a.m_curr_span.data() != b.m_curr_span.data();
}

private:
// iterators must be default-constructible, so include defaults for all
// members
value_type m_span = {};
std::size_t m_width = 0;
std::size_t m_index = 0;
value_type m_curr_span = {};

void update_curr_span() {
if (m_index * m_width + m_width <= m_span.size()) {
m_curr_span = m_span.subspan(m_index * m_width, m_width);
} else {
m_curr_span = {};
}
}

// required for const conversion
friend struct RowIterator<const T>;
};

/**
* Two-dimensional grid of values, indexable by aoc::Pos.
*/
Expand All @@ -310,50 +458,52 @@ struct Grid {
using value_type = T;
using size_type = int;

const size_type height;
const size_type width;
using reference = value_type &;
using const_reference = const value_type &;

protected:
using container_type = std::vector<value_type>;
container_type data;
using iterator = RowIterator<value_type>;
using const_iterator = RowIterator<const value_type>;

public:
using reference = typename container_type::reference;
using const_reference = typename container_type::const_reference;
// work around std::vector<bool> specialization
// thanks to Dwayne Robinson on Stack Overflow for the basic_string idea:
// <https://stackoverflow.com/a/75409768>
using container_type =
std::conditional_t<std::same_as<T, bool>, std::basic_string<value_type>,
std::vector<value_type>>;

using iterator = typename container_type::iterator;
using const_iterator = typename container_type::const_iterator;
const size_type height;
const size_type width;

protected:
constexpr inline std::size_t get_index(size_type x, size_type y) const {
return y * width + x;
}
constexpr Pos index_to_pos(std::size_t index) const {
return Pos(index % width, index / width);
}
container_type m_data;

public:
constexpr Grid(size_type width, size_type height,
const value_type &value = value_type())
: height(height), width(width), data(height * width, value) {}
constexpr Grid(size_type width, size_type height,
const std::vector<value_type> &data)
: height(height), width(width), data(data) {
assert(data.size() == height * width);
: height(height), width(width), m_data(height * width, value) {}
// construct from a flat range
template <util::concepts::any_convertible_range<value_type> R>
constexpr Grid(size_type width, size_type height, R &&range)
: height(height), width(width),
m_data(std::begin(range), std::end(range)) {
assert(static_cast<int>(m_data.size()) == height * width);
}
constexpr Grid(size_type width, size_type height,
std::vector<value_type> &&data)
: height(height), width(width), data(std::move(data)) {
// move-construct from a flat data container
constexpr Grid(size_type width, size_type height, container_type &&data)
: height(height), width(width), m_data(std::move(data)) {
assert(data.size() == height * width);
}
template <util::concepts::const_or_rvalue_ref<
std::vector<std::vector<value_type>>>
V>
// construct from nested ranges
template <std::ranges::range V>
requires std::ranges::range<std::ranges::range_value_t<V>> &&
std::same_as<std::ranges::range_value_t<
std::ranges::range_value_t<V>>,
value_type>
explicit Grid(V &&grid)
: height(grid.size()), width(grid[0].size()), data() {
: height(grid.size()), width(grid[0].size()), m_data() {
assert(!grid.empty());
data.reserve(height * width);
auto data_it = std::back_inserter(data);
m_data.reserve(height * width);
auto data_it = std::back_inserter(m_data);
for (auto &&row : grid) {
assert(static_cast<size_type>(row.size()) == width);
if constexpr (std::is_rvalue_reference_v<V>) {
Expand All @@ -370,13 +520,24 @@ struct Grid {
constexpr Grid &operator=(const Grid &other) = default;
constexpr Grid &operator=(Grid &&other) noexcept = default;

constexpr iterator begin() noexcept { return data.begin(); }
constexpr const_iterator begin() const noexcept { return data.begin(); }
constexpr const_iterator cbegin() const noexcept { return data.cbegin(); }
constexpr inline std::size_t get_index(size_type x, size_type y) const {
return y * width + x;
}
constexpr Pos index_to_pos(std::size_t index) const {
return Pos(index % width, index / width);
}

constexpr iterator end() noexcept { return data.end(); }
constexpr const_iterator end() const noexcept { return data.end(); }
constexpr const_iterator cend() const noexcept { return data.cend(); }
constexpr iterator begin() noexcept { return iterator(m_data, width); }
constexpr const_iterator begin() const noexcept {
return const_iterator(m_data, width);
}
constexpr const_iterator cbegin() const noexcept {
return const_iterator(m_data, width);
}

constexpr iterator end() noexcept { return iterator(); }
constexpr const_iterator end() const noexcept { return const_iterator(); }
constexpr const_iterator cend() const noexcept { return const_iterator(); }

constexpr bool in_bounds(size_type x, size_type y) const noexcept {
return y >= 0 && x >= 0 && y < height && x < width;
Expand All @@ -385,24 +546,38 @@ struct Grid {
return in_bounds(pos.x, pos.y);
}

// indexing by x and y separately
constexpr reference at(size_type x, size_type y) {
return data.at(get_index(x, y));
return m_data.at(get_index(x, y));
}
constexpr reference at(const Pos &pos) { return at(pos.x, pos.y); }

constexpr const_reference at(size_type x, size_type y) const {
return data.at(get_index(x, y));
return m_data.at(get_index(x, y));
}

// indexing by Pos
constexpr reference at(const Pos &pos) { return at(pos.x, pos.y); }
constexpr const_reference at(const Pos &pos) const {
return at(pos.x, pos.y);
}

constexpr reference operator[](const Pos &pos) {
return data[get_index(pos.x, pos.y)];
return m_data[get_index(pos.x, pos.y)];
}
constexpr const_reference operator[](const Pos &pos) const {
return data[get_index(pos.x, pos.y)];
return m_data[get_index(pos.x, pos.y)];
}

// raw indexing into data
constexpr reference at(std::size_t index) { return m_data.at(index); }
constexpr const_reference at(std::size_t index) const {
return m_data.at(index);
}
constexpr reference operator[](std::size_t index) { return m_data[index]; }
constexpr const_reference operator[](std::size_t index) const {
return m_data[index];
}

constexpr container_type &data() { return m_data; }
constexpr const container_type &data() const { return m_data; }
};

template <class T>
Expand Down Expand Up @@ -460,6 +635,34 @@ constexpr bool _grid_lint_helper_constexpr() {
static_assert(std::is_same_v<decltype(grid3a), decltype(grid3b)>);
static_assert(std::is_same_v<decltype(grid3a), decltype(grid3c)>);

static_assert(std::bidirectional_iterator<Grid<int>::iterator>);
static_assert(!std::random_access_iterator<Grid<int>::iterator>);
static_assert(!std::contiguous_iterator<Grid<int>::iterator>);

static_assert(std::bidirectional_iterator<Grid<bool>::iterator>);
static_assert(!std::random_access_iterator<Grid<bool>::iterator>);
static_assert(!std::contiguous_iterator<Grid<bool>::iterator>);

{
int i = 0;
int y = 0;
for (auto &row : grid1a) {
int x = 0;
for (auto &val : row) {
val = i++;
assert(grid1a.at(x, y) == i);
++x;
}
++y;
}
}

// an iterator should be assignable to a const_iterator
static_assert(
std::convertible_to<Grid<int>::iterator, Grid<int>::const_iterator>);
static_assert(
std::convertible_to<Grid<bool>::iterator, Grid<bool>::const_iterator>);

#if __cpp_lib_constexpr_vector
static_assert(_grid_lint_helper_constexpr());
#endif
Expand Down
Loading

0 comments on commit edc3060

Please sign in to comment.