From 71d7227dbc2f91d2603d0ef0924213caa343d882 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 13:32:59 +0200 Subject: [PATCH 01/11] initial flat matrix impl --- include/gl/types/flat_jagged_vector.hpp | 4 +- include/gl/types/flat_matrix.hpp | 553 ++++++++++++++++++++++++ 2 files changed, 555 insertions(+), 2 deletions(-) create mode 100644 include/gl/types/flat_matrix.hpp diff --git a/include/gl/types/flat_jagged_vector.hpp b/include/gl/types/flat_jagged_vector.hpp index e267ddb2..813d299f 100644 --- a/include/gl/types/flat_jagged_vector.hpp +++ b/include/gl/types/flat_jagged_vector.hpp @@ -228,7 +228,7 @@ class flat_jagged_vector { /// @brief Reverse const iterator using const_reverse_iterator = std::reverse_iterator; - // --- constructors --- + // --- constructors and assignment --- /// @brief Default constructor creates an empty `flat_jagged_vector`. /// @post `empty() == true`, `size() == 0`, `data_size() == 0` @@ -330,7 +330,7 @@ class flat_jagged_vector { /// @return `true` if both vectors have the same structure and elements friend bool operator==(const flat_jagged_vector&, const flat_jagged_vector&) = default; - // --- capacity --- + // --- size and capacity --- /// @brief Returns the number of segments in this container. /// @return The count of segments diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp new file mode 100644 index 00000000..45d3b360 --- /dev/null +++ b/include/gl/types/flat_matrix.hpp @@ -0,0 +1,553 @@ +// Copyright (c) 2024-2026 Jakub MusiaƂ +// This file is part of the CPP-GL project (https://github.com/SpectraL519/cpp-gl). +// Licensed under the MIT License. See the LICENSE file in the project root for full license information. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace gl { + +template +class flat_matrix { +public: + using value_type = T; + using size_type = std::size_t; + using reference = value_type&; + using const_reference = const value_type&; + using row_type = std::span; + using const_row_type = std::span; + + // --- iterators --- + + template + class row_iterator { + using data_ptr_type = std::conditional_t; + + public: + using iterator_concept = std::random_access_iterator_tag; + using iterator_category = std::random_access_iterator_tag; + using value_type = std::conditional_t; + using difference_type = std::ptrdiff_t; + using pointer = void; + using reference = value_type; + + row_iterator() = default; + + row_iterator(data_ptr_type data_ptr, size_type row_size, size_type row_idx) noexcept + : _data_ptr(data_ptr), _row_size(row_size), _row_idx(row_idx) {} + + operator row_iterator() const noexcept + requires(not Const) + { + return row_iterator(this->_data_ptr, this->_row_size, this->_row_idx); + } + + [[nodiscard]] reference operator*() const noexcept { + return reference(this->_data_ptr + this->_row_idx * this->_row_size, this->_row_size); + } + + [[nodiscard]] reference operator[](difference_type n) const noexcept { + return *(*this + n); + } + + row_iterator& operator++() noexcept { + ++this->_row_idx; + return *this; + } + + row_iterator operator++(int) noexcept { + auto tmp = *this; + ++this->_row_idx; + return tmp; + } + + row_iterator& operator--() noexcept { + --this->_row_idx; + return *this; + } + + row_iterator operator--(int) noexcept { + auto tmp = *this; + --this->_row_idx; + return tmp; + } + + row_iterator& operator+=(difference_type n) noexcept { + this->_row_idx += n; + return *this; + } + + row_iterator& operator-=(difference_type n) noexcept { + this->_row_idx -= n; + return *this; + } + + [[nodiscard]] friend row_iterator operator+(row_iterator it, difference_type n) noexcept { + return it += n; + } + + [[nodiscard]] friend row_iterator operator+(difference_type n, row_iterator it) noexcept { + return it += n; + } + + [[nodiscard]] friend row_iterator operator-(row_iterator it, difference_type n) noexcept { + return it -= n; + } + + [[nodiscard]] friend difference_type operator-( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx - rhs._row_idx; + } + + [[nodiscard]] friend bool operator==( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx == rhs._row_idx; + } + + [[nodiscard]] friend auto operator<=>( + const row_iterator& lhs, const row_iterator& rhs + ) noexcept { + return lhs._row_idx <=> rhs._row_idx; + } + + private: + data_ptr_type _data_ptr{nullptr}; + size_type _row_size{0uz}; + size_type _row_idx{0uz}; + }; + + using iterator = row_iterator; + using const_iterator = row_iterator; + using reverse_iterator = std::reverse_iterator; + using const_reverse_iterator = std::reverse_iterator; + + // --- constructors and assignment --- + + flat_matrix() = default; + + flat_matrix(const flat_matrix&) = default; + flat_matrix& operator=(const flat_matrix&) = default; + + flat_matrix(flat_matrix&& other) noexcept + : _n_rows(std::exchange(other._n_rows, 0uz)), + _n_cols(std::exchange(other._n_cols, 0uz)), + _data(std::move(other._data)) {} + + flat_matrix& operator=(flat_matrix&& other) noexcept { + if (this != &other) { + this->_n_rows = std::exchange(other._n_rows, 0uz); + this->_n_cols = std::exchange(other._n_cols, 0uz); + this->_data = std::move(other._data); + } + return *this; + } + + ~flat_matrix() = default; + + flat_matrix(size_type n_rows, size_type n_cols, const value_type& initial_value = value_type{}) + : _n_rows(n_rows), _n_cols(n_cols), _data(n_rows * n_cols, initial_value) {} + + flat_matrix(std::initializer_list> ilist) { + this->_n_rows = ilist.size(); + if (this->_n_rows == 0uz) + return; + + this->_n_cols = ilist.begin()->size(); + this->_data.reserve(this->_n_rows * this->_n_cols); + + for (const auto& row : ilist) { + if (row.size() != this->_n_cols) { + throw std::invalid_argument(std::format( + "flat_matrix: row size mismatch in initializer_list (expected {}, got {})", + this->_n_cols, + row.size() + )); + } + this->_data.insert(this->_data.end(), row.begin(), row.end()); + } + } + + template + requires std::ranges::input_range> + and std::convertible_to< + std::ranges::range_reference_t>, + value_type> + explicit flat_matrix(R&& r) { + if constexpr (std::ranges::sized_range) + this->_n_rows = std::ranges::size(r); + + bool first = true; + for (auto&& subrange : r) { + // can't consume an input range + // size has to be determined by consuming the range and counting how many elements were inserted into _data + const auto old_size = this->_data.size(); + + if constexpr (std::ranges::contiguous_range) { + auto* ptr = std::ranges::data(subrange); + const auto dist = std::ranges::size(subrange); + this->_data.insert(this->_data.end(), ptr, ptr + dist); + } + else { + this->_data.insert( + this->_data.end(), std::ranges::begin(subrange), std::ranges::end(subrange) + ); + } + + const auto row_size = this->_data.size() - old_size; + if (first) { + this->_n_cols = row_size; + + if constexpr (std::ranges::sized_range< + R>) // prevent reallocation during loop for sized range + this->_data.reserve(this->_n_rows * this->_n_cols); + + first = false; + } + else if (row_size != this->_n_cols) { + throw std::invalid_argument(std::format( + "flat_matrix: row size mismatch in range constructor (expected {}, got {})", + this->_n_cols, + row_size + )); + } + } + + if constexpr (not std::ranges::sized_range< + R>) // calculate the number of rows for an unsized range + this->_n_rows = this->_data.size() / (this->_n_cols > 0uz ? this->_n_cols : 1uz); + } + + // --- comparison --- + + friend bool operator==(const flat_matrix&, const flat_matrix&) = default; + + // --- size and capacity --- + + [[nodiscard]] size_type size() const noexcept { + return this->_n_rows; + } + + [[nodiscard]] size_type n_rows() const noexcept { + return this->_n_rows; + } + + [[nodiscard]] size_type n_cols() const noexcept { + return this->_n_cols; + } + + [[nodiscard]] bool empty() const noexcept { + return this->_data.empty(); + } + + [[nodiscard]] size_type data_capacity() const noexcept { + return this->_data.capacity(); + } + + void reserve_data(size_type n) { + this->_data.reserve(n); + } + + void shrink_to_fit() { + this->_data.shrink_to_fit(); + } + + void resize(size_type new_rows, size_type new_cols, const value_type& value = value_type{}) { + if (new_rows == this->_n_rows and new_cols == this->_n_cols) + return; + + if (new_cols == this->_n_cols) { + this->_data.resize(new_rows * new_cols, value); + this->_n_rows = new_rows; + return; + } + + std::vector new_data(new_rows * new_cols, value); + const auto min_rows = std::min(this->_n_rows, new_rows); + const auto min_cols = std::min(this->_n_cols, new_cols); + + for (auto r = 0uz; r < min_rows; ++r) + for (auto c = 0uz; c < min_cols; ++c) + new_data[r * new_cols + c] = std::move(this->_data[r * this->_n_cols + c]); + + this->_data = std::move(new_data); + this->_n_rows = new_rows; + this->_n_cols = new_cols; + } + + void clear() { + this->_data.clear(); + this->_n_rows = 0uz; + this->_n_cols = 0uz; + } + + // --- accessors --- + + [[nodiscard]] constexpr size_type index(size_type r, size_type c) const noexcept { + return r * this->_n_cols + c; + } + + [[nodiscard]] row_type operator[](size_type r) { + return row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + } + + [[nodiscard]] const_row_type operator[](size_type r) const { + return const_row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); + } + + [[nodiscard]] reference operator[](size_type r, size_type c) { + return this->_data[this->index(r, c)]; + } + + [[nodiscard]] const_reference operator[](size_type r, size_type c) const { + return this->_data[this->index(r, c)]; + } + + [[nodiscard]] row_type at(size_type r) { + this->_check_row(r); + return (*this)[r]; + } + + [[nodiscard]] const_row_type at(size_type r) const { + this->_check_row(r); + return (*this)[r]; + } + + [[nodiscard]] reference at(size_type r, size_type c) { + this->_check_bounds(r, c); + return (*this)[r, c]; + } + + [[nodiscard]] const_reference at(size_type r, size_type c) const { + this->_check_bounds(r, c); + return (*this)[r, c]; + } + + [[nodiscard]] row_type front() noexcept { + return (*this)[0uz]; + } + + [[nodiscard]] const_row_type front() const noexcept { + return (*this)[0uz]; + } + + [[nodiscard]] row_type back() noexcept { + return (*this)[this->_n_rows - 1uz]; + } + + [[nodiscard]] const_row_type back() const noexcept { + return (*this)[this->_n_rows - 1uz]; + } + + [[nodiscard]] reference front(size_type r) noexcept { + return (*this)[r, 0uz]; + } + + [[nodiscard]] const_reference front(size_type r) const noexcept { + return (*this)[r, 0uz]; + } + + [[nodiscard]] reference back(size_type r) noexcept { + return (*this)[r, this->_n_cols - 1uz]; + } + + [[nodiscard]] const_reference back(size_type r) const noexcept { + return (*this)[r, this->_n_cols - 1uz]; + } + + [[nodiscard]] auto rows() noexcept { + return std::views::iota(size_type{0}, this->_n_rows) + | std::views::transform([this](size_type i) -> row_type { return (*this)[i]; }); + } + + [[nodiscard]] auto rows() const noexcept { + return std::views::iota(size_type{0}, this->_n_rows) + | std::views::transform([this](size_type i) -> const_row_type { return (*this)[i]; }); + } + + [[nodiscard]] size_type data_size() const noexcept { + return this->_data.size(); + } + + [[nodiscard]] row_type data_view() noexcept { + return row_type(this->_data); + } + + [[nodiscard]] const_row_type data_view() const noexcept { + return const_row_type(this->_data); + } + + [[nodiscard]] std::vector& data_storage() noexcept { + return this->_data; + } + + [[nodiscard]] const std::vector& data_storage() const noexcept { + return this->_data; + } + + [[nodiscard]] value_type* data_ptr() noexcept { + return this->_data.data(); + } + + [[nodiscard]] const value_type* data_ptr() const noexcept { + return this->_data.data(); + } + + // --- iterators --- + + [[nodiscard]] iterator begin() noexcept { + return iterator(this->_data.data(), this->_n_cols, 0uz); + } + + [[nodiscard]] iterator end() noexcept { + return iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + [[nodiscard]] const_iterator begin() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, 0uz); + } + + [[nodiscard]] const_iterator end() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + [[nodiscard]] const_iterator cbegin() const noexcept { + return this->begin(); + } + + [[nodiscard]] const_iterator cend() const noexcept { + return this->end(); + } + + [[nodiscard]] reverse_iterator rbegin() noexcept { + return reverse_iterator(this->end()); + } + + [[nodiscard]] reverse_iterator rend() noexcept { + return reverse_iterator(this->begin()); + } + + [[nodiscard]] const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(this->end()); + } + + [[nodiscard]] const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(this->begin()); + } + + [[nodiscard]] const_reverse_iterator crbegin() const noexcept { + return this->rbegin(); + } + + [[nodiscard]] const_reverse_iterator crend() const noexcept { + return this->rend(); + } + + // --- modifiers --- + + template + requires std::convertible_to, value_type> + void push_back(R&& r) { + if constexpr (std::ranges::sized_range) { + const auto row_size = static_cast(std::ranges::size(r)); + + if (this->_n_rows > 0uz and row_size != this->_n_cols) + throw std::invalid_argument(std::format( + "flat_matrix::push_back: row size mismatch (expected {}, got {})", + this->_n_cols, + row_size + )); + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_cols = row_size; + + this->_data.reserve(this->_data.size() + row_size); + + if constexpr (std::ranges::contiguous_range) { + auto* ptr = std::ranges::data(r); + this->_data.insert(this->_data.end(), ptr, ptr + row_size); + } + else { + this->_data.insert(this->_data.end(), std::ranges::begin(r), std::ranges::end(r)); + } + } + else { + // can't consume an input range + // size has to be determined by consuming the range and counting how many elements were inserted into _data + const auto old_size = this->_data.size(); + + this->_data.insert(this->_data.end(), std::ranges::begin(r), std::ranges::end(r)); + + const auto row_size = this->_data.size() - old_size; + + if (this->_n_rows > 0uz and row_size != this->_n_cols) { + this->_data.resize(old_size + ); // rollback the insertion for strong exception guarantee + throw std::invalid_argument(std::format( + "flat_matrix::push_back: row size mismatch (expected {}, got {})", + this->_n_cols, + row_size + )); + } + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_cols = + row_size; // set the number of columns based on the first inserted row for an unsized range + } + + ++this->_n_rows; + } + + void push_back(std::initializer_list ilist) { + this->push_back(std::span{ilist}); + } + + void pop_back() { + if (this->empty()) + return; + + this->_data.resize(this->_data.size() - this->_n_cols); + --this->_n_rows; + + if (this->_n_rows == 0uz) + this->_n_cols = 0uz; + } + +private: + void _check_row(size_type r) const { + if (r >= this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::_check_row: r (which is {}) >= this->n_rows() (which is {})", + r, + this->_n_rows + )); + } + } + + void _check_bounds(size_type r, size_type c) const { + this->_check_row(r); + if (c >= this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::_check_bounds: c (which is {}) >= this->n_cols() (which is {})", + c, + this->_n_cols + )); + } + } + + size_type _n_rows{0uz}; + size_type _n_cols{0uz}; + std::vector _data; +}; + +} // namespace gl From 05d60f6e5925e52a21e6a1de4e252401cc5607cc Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 14:07:36 +0200 Subject: [PATCH 02/11] refined insertion and deletion methods --- include/gl/types/flat_matrix.hpp | 247 ++++++++++++++++++++++++++++--- 1 file changed, 223 insertions(+), 24 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 45d3b360..c56b78cf 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -324,12 +324,14 @@ class flat_matrix { } [[nodiscard]] reference at(size_type r, size_type c) { - this->_check_bounds(r, c); + this->_check_row(r); + this->_check_col(c); return (*this)[r, c]; } [[nodiscard]] const_reference at(size_type r, size_type c) const { - this->_check_bounds(r, c); + this->_check_row(r); + this->_check_col(c); return (*this)[r, c]; } @@ -453,20 +455,44 @@ class flat_matrix { return this->rend(); } - // --- modifiers --- + // --- modifiers (rows) --- template requires std::convertible_to, value_type> - void push_back(R&& r) { + void push_row(R&& r) { + this->insert_row(this->_n_rows, std::forward(r)); + } + + void push_row(std::initializer_list ilist) { + this->insert_row(this->_n_rows, std::span{ilist}); + } + + void push_row(const value_type& value) { + this->insert_row(this->_n_rows, value); + } + + template + requires std::convertible_to, value_type> + void insert_row(size_type pos, R&& r) { + if (pos > this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::insert_row: pos (which is {}) > this->rows() (which is {})", + pos, + this->_n_rows + )); + } + + const auto insert_idx = pos * this->_n_cols; + if constexpr (std::ranges::sized_range) { const auto row_size = static_cast(std::ranges::size(r)); - - if (this->_n_rows > 0uz and row_size != this->_n_cols) + if (this->_n_rows > 0uz and row_size != this->_n_cols) { throw std::invalid_argument(std::format( - "flat_matrix::push_back: row size mismatch (expected {}, got {})", + "flat_matrix::insert_row: row size mismatch (expected {}, got {})", this->_n_cols, row_size )); + } if (this->_n_rows == 0uz and this->_n_cols == 0uz) this->_n_cols = row_size; @@ -475,44 +501,59 @@ class flat_matrix { if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); - this->_data.insert(this->_data.end(), ptr, ptr + row_size); + this->_data.insert(this->_data.begin() + insert_idx, ptr, ptr + row_size); } else { - this->_data.insert(this->_data.end(), std::ranges::begin(r), std::ranges::end(r)); + this->_data.insert( + this->_data.begin() + insert_idx, std::ranges::begin(r), std::ranges::end(r) + ); } } else { - // can't consume an input range - // size has to be determined by consuming the range and counting how many elements were inserted into _data + // single-pass input range: insert, validate size, rollback if mismatched (strong exception guarantee) const auto old_size = this->_data.size(); - this->_data.insert(this->_data.end(), std::ranges::begin(r), std::ranges::end(r)); + this->_data.insert( + this->_data.begin() + insert_idx, std::ranges::begin(r), std::ranges::end(r) + ); const auto row_size = this->_data.size() - old_size; - if (this->_n_rows > 0uz and row_size != this->_n_cols) { - this->_data.resize(old_size - ); // rollback the insertion for strong exception guarantee + this->_data.erase( + this->_data.begin() + insert_idx, this->_data.begin() + insert_idx + row_size + ); throw std::invalid_argument(std::format( - "flat_matrix::push_back: row size mismatch (expected {}, got {})", + "flat_matrix::insert_row: row size mismatch (expected {}, got {})", this->_n_cols, row_size )); } if (this->_n_rows == 0uz and this->_n_cols == 0uz) - this->_n_cols = - row_size; // set the number of columns based on the first inserted row for an unsized range + this->_n_cols = row_size; } ++this->_n_rows; } - void push_back(std::initializer_list ilist) { - this->push_back(std::span{ilist}); + void insert_row(size_type pos, std::initializer_list ilist) { + this->insert_row(pos, std::span{ilist}); } - void pop_back() { + void insert_row(size_type pos, const value_type& value) { + if (pos > this->_n_rows) { + throw std::out_of_range(std::format( + "flat_matrix::insert_row: pos (which is {}) > this->rows() (which is {})", + pos, + this->_n_rows + )); + } + + this->_data.insert(this->_data.begin() + (pos * this->_n_cols), this->_n_cols, value); + ++this->_n_rows; + } + + void pop_row() { if (this->empty()) return; @@ -523,6 +564,165 @@ class flat_matrix { this->_n_cols = 0uz; } + void erase_row(size_type pos) { + this->_check_row(pos); + if (this->_n_rows == 1uz) { + this->clear(); + return; + } + + const auto start_it = this->_data.begin() + (pos * this->_n_cols); + this->_data.erase(start_it, start_it + this->_n_cols); + --this->_n_rows; + } + + // --- modifiers (columns) --- + + template + requires std::convertible_to, value_type> + void push_col(R&& r) { + this->insert_col(this->_n_cols, std::forward(r)); + } + + void push_col(std::initializer_list ilist) { + this->insert_col(this->_n_cols, std::span{ilist}); + } + + void push_col(const value_type& value) { + this->insert_col(this->_n_cols, value); + } + + template + requires std::convertible_to, value_type> + void insert_col(size_type pos, R&& r) { + if (pos > this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::insert_col: pos (which is {}) > this->cols() (which is {})", + pos, + this->_n_cols + )); + } + + if constexpr (std::ranges::sized_range) { + const auto col_size = static_cast(std::ranges::size(r)); + if (this->_n_cols > 0uz and col_size != this->_n_rows) { + throw std::invalid_argument(std::format( + "flat_matrix::insert_col: col size mismatch (expected {}, got {})", + this->_n_rows, + col_size + )); + } + + if (this->_n_rows == 0uz and this->_n_cols == 0uz) + this->_n_rows = col_size; + + // pre-allocate new vector to guarantee O(V^2) structural shift, avoiding O(V^3) with multiple inserts + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); + + auto r_it = std::ranges::begin(r); + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + // move old row elements up to insertion point + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + // insert new column element + new_data.push_back(*r_it++); + // move the remainder of old row + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + ++this->_n_cols; + } + else { + // create a temporary sized range and recursively call the method to leverage the sized range logic, + // ensuring strong exception guarantee for unsized input + this->insert_col(pos, std::ranges::to>(std::forward(r))); + } + } + + void insert_col(size_type pos, std::initializer_list ilist) { + this->insert_col(pos, std::span{ilist}); + } + + void insert_col(size_type pos, const value_type& value) { + if (pos > this->_n_cols) { + throw std::out_of_range(std::format( + "flat_matrix::insert_col: pos (which is {}) > this->cols() (which is {})", + pos, + this->_n_cols + )); + } + + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); + + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + new_data.push_back(value); // Insert the fill value + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + ++this->_n_cols; + } + + void pop_col() { + if (this->empty() || this->_n_cols == 0uz) + return; + + this->erase_col(this->_n_cols - 1uz); + } + + void erase_col(size_type pos) { + this->_check_col(pos); + + if (this->_n_cols == 1uz) { + this->clear(); + return; + } + + std::vector new_data; + new_data.reserve(this->_n_rows * (this->_n_cols - 1uz)); + + for (size_type r_idx = 0uz; r_idx < this->_n_rows; ++r_idx) { + auto row_begin = this->_data.begin() + r_idx * this->_n_cols; + + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin), + std::make_move_iterator(row_begin + pos) + ); + new_data.insert( + new_data.end(), + std::make_move_iterator(row_begin + pos + 1uz), + std::make_move_iterator(row_begin + this->_n_cols) + ); + } + + this->_data = std::move(new_data); + --this->_n_cols; + } + private: void _check_row(size_type r) const { if (r >= this->_n_rows) { @@ -534,11 +734,10 @@ class flat_matrix { } } - void _check_bounds(size_type r, size_type c) const { - this->_check_row(r); + void _check_col(size_type c) const { if (c >= this->_n_cols) { throw std::out_of_range(std::format( - "flat_matrix::_check_bounds: c (which is {}) >= this->n_cols() (which is {})", + "flat_matrix::_check_col: c (which is {}) >= this->n_cols() (which is {})", c, this->_n_cols )); From 1f14e9052b9217dd2e322b270af1584ebe15b864 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 19:39:02 +0200 Subject: [PATCH 03/11] flat matrix ctor tests --- include/gl/types/flat_matrix.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index c56b78cf..453d7b8a 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -155,8 +155,8 @@ class flat_matrix { ~flat_matrix() = default; - flat_matrix(size_type n_rows, size_type n_cols, const value_type& initial_value = value_type{}) - : _n_rows(n_rows), _n_cols(n_cols), _data(n_rows * n_cols, initial_value) {} + flat_matrix(size_type n_rows, size_type n_cols, const value_type& value = value_type{}) + : _n_rows(n_rows), _n_cols(n_cols), _data(n_rows * n_cols, value) {} flat_matrix(std::initializer_list> ilist) { this->_n_rows = ilist.size(); @@ -208,8 +208,8 @@ class flat_matrix { if (first) { this->_n_cols = row_size; - if constexpr (std::ranges::sized_range< - R>) // prevent reallocation during loop for sized range + // prevent reallocation during loop for sized range + if constexpr (std::ranges::sized_range) this->_data.reserve(this->_n_rows * this->_n_cols); first = false; @@ -223,8 +223,8 @@ class flat_matrix { } } - if constexpr (not std::ranges::sized_range< - R>) // calculate the number of rows for an unsized range + // calculate the number of rows for an unsized range + if constexpr (not std::ranges::sized_range) this->_n_rows = this->_data.size() / (this->_n_cols > 0uz ? this->_n_cols : 1uz); } From 13ff2ff36dad34ff209ea785b4443c955fdd83d4 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 19:43:10 +0200 Subject: [PATCH 04/11] flat matrix comparison tests --- tests/source/gl/test_flat_jagged_vector.cpp | 28 +-- tests/source/gl/test_flat_matrix.cpp | 250 ++++++++++++++++++++ 2 files changed, 264 insertions(+), 14 deletions(-) create mode 100644 tests/source/gl/test_flat_matrix.cpp diff --git a/tests/source/gl/test_flat_jagged_vector.cpp b/tests/source/gl/test_flat_jagged_vector.cpp index fe5554c3..1aa26cd8 100644 --- a/tests/source/gl/test_flat_jagged_vector.cpp +++ b/tests/source/gl/test_flat_jagged_vector.cpp @@ -162,47 +162,47 @@ struct test_flat_jagged_vector_comparison { TEST_CASE_FIXTURE( test_flat_jagged_vector_comparison, - "equality operator should return true for equal segment_vectors" + "equality operator should return true for equal flat_jagged_vectors" ) { - sut_type sv1{ + sut_type jv1{ {1, 2}, {3, 4} }; - sut_type sv2{ + sut_type jv2{ {1, 2}, {3, 4} }; - CHECK_EQ(sv1, sv2); + CHECK_EQ(jv1, jv2); } TEST_CASE_FIXTURE( test_flat_jagged_vector_comparison, - "inequality operator should return true for different segment_vectors" + "inequality operator should return true for different flat_jagged_vectors" ) { - sut_type sv1{ + sut_type jv1{ {1, 2}, {3, 4} }; - sut_type sv2{ + sut_type jv2{ {1, 2}, {3, 5} }; - sut_type sv3{ + sut_type jv3{ {1, 2}, {3, 4}, {5} }; - CHECK_NE(sv1, sv2); - CHECK_NE(sv1, sv3); + CHECK_NE(jv1, jv2); + CHECK_NE(jv1, jv3); } -TEST_CASE_FIXTURE(test_flat_jagged_vector_comparison, "empty segment_vectors should be equal") { - sut_type sv1; - sut_type sv2; +TEST_CASE_FIXTURE(test_flat_jagged_vector_comparison, "empty flat_jagged_vectors should be equal") { + sut_type jv1; + sut_type jv2; - CHECK_EQ(sv1, sv2); + CHECK_EQ(jv1, jv2); } struct test_flat_jagged_vector_capacity { diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp new file mode 100644 index 00000000..c606d4db --- /dev/null +++ b/tests/source/gl/test_flat_matrix.cpp @@ -0,0 +1,250 @@ +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace gl_testing { + +TEST_SUITE_BEGIN("test_flat_matrix"); + +struct test_flat_matrix_constructors { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "default constructor should create empty flat_matrix" +) { + sut_type sut; + CHECK(sut.empty()); + CHECK_EQ(sut.size(), 0uz); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + CHECK_EQ(sut.data_size(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "copy constructor should create an equal copy") { + sut_type original; + original.push_row({1, 2, 3}); + original.push_row({4, 5, 6}); + + sut_type copy = original; + + CHECK_EQ(copy, original); + CHECK_EQ(copy.size(), 2uz); + CHECK_EQ(copy.n_rows(), 2uz); + CHECK_EQ(copy.n_cols(), 3uz); + CHECK_EQ(copy.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "copy assignment should create an equal copy") { + sut_type original; + original.push_row({1, 2, 3}); + original.push_row({4, 5, 6}); + + sut_type dest; + dest = original; + + CHECK_EQ(dest, original); + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "move constructor should transfer ownership") { + sut_type source; + source.push_row({1, 2, 3}); + source.push_row({4, 5, 6}); + + sut_type dest = std::move(source); + + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); + CHECK(source.empty()); + CHECK_EQ(source.n_rows(), 0uz); + CHECK_EQ(source.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_constructors, "move assignment should transfer ownership") { + sut_type source; + source.push_row({1, 2, 3}); + source.push_row({4, 5, 6}); + + sut_type dest; + dest = std::move(source); + + CHECK_EQ(dest.size(), 2uz); + CHECK_EQ(dest.n_rows(), 2uz); + CHECK_EQ(dest.n_cols(), 3uz); + CHECK_EQ(dest.data_size(), 6uz); + CHECK(source.empty()); + CHECK_EQ(source.n_rows(), 0uz); + CHECK_EQ(source.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "move assignment should handle self-assignment correctly" +) { + sut_type sut; + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + + sut = std::move(sut); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "(n_rows, n_cols) constructor should build a matrixed filled with a default type value" +) { + sut_type sut(2uz, 3uz); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::all_of(sut.data_view(), [](const auto v) { return v == int{}; })); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "(n_rows, n_cols, value) constructor should build a matrixed filled with the given value" +) { + int fill_value = 123; + sut_type sut(2uz, 3uz, fill_value); + + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::all_of(sut.data_view(), [fill_value](const auto v) { + return v == fill_value; + })); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "initializer list constructor should initialize matrix for same-size rows" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, + "initializer list constructor should throw for different size rows" +) { + const auto create_sut = []() { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7} + }; + }; + CHECK_THROWS_AS(create_sut(), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "range constructor should initialize matrix for same-size rows" +) { + std::vector> data{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + sut_type sut{std::move(data)}; + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_constructors, "range constructor should throw for different size rows" +) { + std::vector> data{ + {1, 2, 3}, + {4, 5, 6}, + {7} + }; + const auto create_sut = [&data]() { sut_type sut{std::move(data)}; }; + CHECK_THROWS_AS(create_sut(), std::invalid_argument); +} + +struct test_flat_matrix_comparison { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_comparison, "equality operator should return true for equal matrices" +) { + sut_type m1{ + {1, 2}, + {3, 4} + }; + sut_type m2{ + {1, 2}, + {3, 4} + }; + + CHECK_EQ(m1, m2); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_comparison, "inequality operator should return true for different matrices" +) { + sut_type m1{ + {1, 2}, + {3, 4} + }; + sut_type m2{ + {1, 2}, + {3, 5} + }; + sut_type m3{ + {1, 2}, + {3, 4}, + {5, 6} + }; + + CHECK_NE(m1, m2); + CHECK_NE(m1, m3); +} + +TEST_CASE_FIXTURE(test_flat_matrix_comparison, "empty matrices should be equal") { + sut_type m1; + sut_type m2; + + CHECK_EQ(m1, m2); +} + +TEST_SUITE_END(); // test_flat_matrix + +} // namespace gl_testing From ef36544a9998ea3d1954b845f6818587ab532799 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 20:02:57 +0200 Subject: [PATCH 05/11] flat matrix capacity tests --- tests/source/gl/test_flat_matrix.cpp | 229 +++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index c606d4db..32f72765 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -245,6 +245,235 @@ TEST_CASE_FIXTURE(test_flat_matrix_comparison, "empty matrices should be equal") CHECK_EQ(m1, m2); } +struct test_flat_matrix_capacity { + using sut_type = gl::flat_matrix; + sut_type sut; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "size and n_rows should return the number of rows and n_cols should return the number of " + "columns" +) { + CHECK_EQ(sut.size(), 0uz); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + + sut.push_row({1, 2, 3}); + CHECK_EQ(sut.size(), 1uz); + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + + sut.push_row({4, 5, 6}); + CHECK_EQ(sut.size(), 2uz); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + + sut.push_row({7, 8, 9}); + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "empty should return true only when there are no elements in the matrix" +) { + CHECK(sut.empty()); + + sut.push_row({1, 2, 3}); + CHECK_FALSE(sut.empty()); + + sut.pop_row(); + CHECK(sut.empty()); + + sut.push_col({1, 2, 3}); + CHECK_FALSE(sut.empty()); + + sut.pop_col(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "reserve_data should reserve space for elements") { + sut.reserve_data(10uz); + CHECK_EQ(sut.data_capacity(), 10uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "shrink_to_fit should reduce capacity") { + sut.push_row({1, 2, 3}); + sut.shrink_to_fit(); + + CHECK_EQ(sut.data_capacity(), 3uz); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should shrink container when n_rows < current n_rows" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(2uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should shrink container when c_cols < current n_cols" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 2uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), 6uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should grow container with the fiven value when n_rows > current " + "n_rows" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(4uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 4uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 12uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-1, -1, -1})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should grow container with default type value when n_cols > current " + "n_cols" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 4uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 12uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should properly change the dimensions of the matrix for mixed size " + "differences" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(4uz, 4uz, -1); + + CHECK_EQ(sut.n_rows(), 4uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 16uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-1, -1, -1, -1})); + + sut.resize(3uz, 5uz, -2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 5uz); + CHECK_EQ(sut.data_size(), 15uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3, -1, -2})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6, -1, -2})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9, -1, -2})); + + sut.resize(5uz, 3uz, -3); + + CHECK_EQ(sut.n_rows(), 5uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 15uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); + CHECK(std::ranges::equal(sut[3uz], std::vector{-3, -3, -3})); + CHECK(std::ranges::equal(sut[4uz], std::vector{-3, -3, -3})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_capacity, + "resize(n_rows, n_cols, v) should do nothing when dimensions are not changed" +) { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + sut.push_row({7, 8, 9}); + + REQUIRE_EQ(sut.n_rows(), 3uz); + REQUIRE_EQ(sut.n_cols(), 3uz); + REQUIRE_EQ(sut.data_size(), 9uz); + + sut.resize(3uz, 3uz, -1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 9uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{1, 2, 3})); + CHECK(std::ranges::equal(sut[1uz], std::vector{4, 5, 6})); + CHECK(std::ranges::equal(sut[2uz], std::vector{7, 8, 9})); +} + +TEST_CASE_FIXTURE(test_flat_matrix_capacity, "clear should remove all data") { + sut.push_row({1, 2, 3}); + sut.push_row({4, 5, 6}); + + sut.clear(); + + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); + CHECK_EQ(sut.data_size(), 0uz); +} + TEST_SUITE_END(); // test_flat_matrix } // namespace gl_testing From 9ded925e1742d81126c489c710bc455f7002c100 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 20:25:52 +0200 Subject: [PATCH 06/11] flat matrix accessors tests --- tests/source/gl/test_flat_jagged_vector.cpp | 70 ++-- tests/source/gl/test_flat_matrix.cpp | 338 ++++++++++++++++++++ 2 files changed, 379 insertions(+), 29 deletions(-) diff --git a/tests/source/gl/test_flat_jagged_vector.cpp b/tests/source/gl/test_flat_jagged_vector.cpp index 1aa26cd8..53a956a3 100644 --- a/tests/source/gl/test_flat_jagged_vector.cpp +++ b/tests/source/gl/test_flat_jagged_vector.cpp @@ -385,7 +385,7 @@ TEST_CASE_FIXTURE(test_flat_jagged_vector_capacity, "clear should remove all seg CHECK_EQ(sut.data_size(), 0uz); } -struct test_segment_vector_accessors { +struct test_flat_jagged_vector_accessors { using sut_type = gl::flat_jagged_vector; sut_type sut{ @@ -406,7 +406,9 @@ struct test_segment_vector_accessors { std::size_t dummy_offset = 999uz; }; -TEST_CASE_FIXTURE(test_segment_vector_accessors, "operator[] should return segment at given index") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "operator[] should return segment at given index" +) { auto seg0 = sut[0uz]; CHECK(std::ranges::equal(seg0, data[0uz])); seg0.front() = dummy_value; @@ -424,7 +426,7 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "operator[] should return segme } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const operator[] should return const segment at given index" + test_flat_jagged_vector_accessors, "const operator[] should return const segment at given index" ) { const auto& const_sut = sut; @@ -435,7 +437,7 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::equal(seg1, data[1uz])); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should return segment at given index") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "at() should return segment at given index") { auto seg0 = sut.at(0uz); CHECK(std::ranges::equal(seg0, data[0uz])); seg0.front() = dummy_value; @@ -452,13 +454,13 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should return segment at CHECK_EQ(sut.at(2uz).front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "at() should throw for out of range index") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "at() should throw for out of range index") { CHECK_THROWS_AS(static_cast(sut.at(3uz)), std::out_of_range); CHECK_THROWS_AS(static_cast(sut.at(10uz)), std::out_of_range); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const at() should return const segment at given index" + test_flat_jagged_vector_accessors, "const at() should return const segment at given index" ) { const auto& const_sut = sut; @@ -472,13 +474,17 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::equal(seg2, data[2uz])); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const at() should throw for out of range index") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const at() should throw for out of range index" +) { const auto& const_sut = sut; CHECK_THROWS_AS(static_cast(const_sut.at(3uz)), std::out_of_range); CHECK_THROWS_AS(static_cast(const_sut.at(10uz)), std::out_of_range); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "segments() should return a view of all segments") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "segments() should return a view of all segments" +) { auto n_segments = 0uz; for (auto seg : sut.segments()) { CHECK(std::ranges::equal(seg, data[n_segments])); @@ -493,7 +499,7 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "segments() should return a vie } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const segments() should return a const view of all segments" + test_flat_jagged_vector_accessors, "const segments() should return a const view of all segments" ) { const auto& const_sut = sut; @@ -507,7 +513,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "empty(i) should return true for empty segments and false for non-empty segments" ) { CHECK_FALSE(sut.empty(0uz)); @@ -519,7 +525,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "segment_size(i) should return the size of a segment" + test_flat_jagged_vector_accessors, "segment_size(i) should return the size of a segment" ) { CHECK_EQ(sut.segment_size(0uz), 3uz); CHECK_EQ(sut.segment_size(1uz), 2uz); @@ -527,26 +533,28 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "data_size() should return the total number of elements" + test_flat_jagged_vector_accessors, "data_size() should return the total number of elements" ) { CHECK_EQ(sut.data_size(), 6uz); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "data_view() should return a span of all data") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "data_view() should return a span of all data" +) { CHECK(std::ranges::equal(sut.data_view(), flat_data)); sut.data_view().front() = dummy_value; CHECK_EQ(sut.data_view().front(), dummy_value); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const data_view() should return a const span of all data" + test_flat_jagged_vector_accessors, "const data_view() should return a const span of all data" ) { const auto& const_sut = sut; CHECK(std::ranges::equal(const_sut.data_view(), flat_data)); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "data_storage() should return a mutable reference to the internal vector" ) { auto& storage_ref = sut.data_storage(); @@ -559,7 +567,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const data_storage() should return a const reference to the internal vector" ) { const auto& const_sut = sut; @@ -570,7 +578,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "data_ptr() should return a mutable raw pointer to the first element" ) { auto* ptr = sut.data_ptr(); @@ -584,7 +592,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const data_ptr() should return a const raw pointer to the first element" ) { const auto& const_sut = sut; @@ -595,7 +603,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "offsets_view() should return a span of all offsets" + test_flat_jagged_vector_accessors, "offsets_view() should return a span of all offsets" ) { CHECK(std::ranges::equal(sut.offsets_view(), offsets)); sut.offsets_view().front() = dummy_offset; @@ -603,14 +611,15 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, "const offsets_view() should return a const span of all offsets" + test_flat_jagged_vector_accessors, + "const offsets_view() should return a const span of all offsets" ) { const auto& const_sut = sut; CHECK(std::ranges::equal(const_sut.offsets_view(), offsets)); } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "offsets_storage() should return a mutable reference to the internal vector" ) { auto& storage_ref = sut.offsets_storage(); @@ -623,7 +632,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const offsets_storage() should return a const reference to the internal vector" ) { const auto& const_sut = sut; @@ -634,7 +643,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "offsets_ptr() should return a mutable raw pointer to the first element" ) { auto* ptr = sut.offsets_ptr(); @@ -648,7 +657,7 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_segment_vector_accessors, + test_flat_jagged_vector_accessors, "const offsets_ptr() should return a const raw pointer to the first element" ) { const auto& const_sut = sut; @@ -658,7 +667,7 @@ TEST_CASE_FIXTURE( CHECK(std::equal(ptr, ptr + const_sut.offsets_view().size(), offsets.begin())); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "front() should return the first segment") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "front() should return the first segment") { auto front_seg = sut.front(); CHECK(std::ranges::equal(front_seg, data.front())); @@ -666,13 +675,15 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "front() should return the firs CHECK_EQ(sut.front().front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const front() should return const first segment") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const front() should return const first segment" +) { const auto& const_sut = sut; auto front_seg = const_sut.front(); CHECK(std::ranges::equal(front_seg, data.front())); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "back() should return the last segment") { +TEST_CASE_FIXTURE(test_flat_jagged_vector_accessors, "back() should return the last segment") { auto back_seg = sut.back(); CHECK(std::ranges::equal(back_seg, data.back())); @@ -680,7 +691,9 @@ TEST_CASE_FIXTURE(test_segment_vector_accessors, "back() should return the last CHECK_EQ(sut.back().front(), dummy_value); } -TEST_CASE_FIXTURE(test_segment_vector_accessors, "const back() should return const last segment") { +TEST_CASE_FIXTURE( + test_flat_jagged_vector_accessors, "const back() should return const last segment" +) { const auto& const_sut = sut; auto back_seg = const_sut.back(); CHECK(std::ranges::equal(back_seg, data.back())); @@ -711,7 +724,6 @@ TEST_CASE_FIXTURE( sut[0uz, 1uz] = dummy_value; CHECK_EQ(sut[0uz, 1uz], dummy_value); - CHECK_EQ(sut[0uz, 2uz], seg0[2uz]); sut[0uz, 2uz] = dummy_value; CHECK_EQ(sut[0uz, 2uz], dummy_value); diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index 32f72765..cebd2147 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -474,6 +474,344 @@ TEST_CASE_FIXTURE(test_flat_matrix_capacity, "clear should remove all data") { CHECK_EQ(sut.data_size(), 0uz); } +struct test_flat_matrix_accessors { + using sut_type = gl::flat_matrix; + + sut_type sut{ + {1, 2, 3}, + {4, 5, 6} + }; + + std::vector sut_row0{1, 2, 3}; + std::vector sut_row1{4, 5, 6}; + + std::vector> data{sut_row0, sut_row1}; + std::vector flat_data{1, 2, 3, 4, 5, 6}; + + std::size_t sut_n_rows = 2uz; + std::size_t sut_n_cols = 3uz; + + int dummy_value = 111; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "operator[r] should return row at given index") { + auto r0 = sut[0uz]; + CHECK(std::ranges::equal(r0, data[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut[0uz].front(), dummy_value); + + auto r1 = sut[1uz]; + CHECK(std::ranges::equal(r1, data[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut[1uz].front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const operator[r] should return const row at given index" +) { + const auto& const_sut = sut; + + auto r0 = const_sut[0uz]; + CHECK(std::ranges::equal(r0, data[0uz])); + + auto r1 = const_sut[1uz]; + CHECK(std::ranges::equal(r1, data[1uz])); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "operator[](r, r) should return element at given row and position" +) { + CHECK_EQ(sut[0uz, 0uz], sut_row0[0uz]); + sut[0uz, 0uz] = dummy_value; + CHECK_EQ(sut[0uz, 0uz], dummy_value); + + CHECK_EQ(sut[0uz, 1uz], sut_row0[1uz]); + sut[0uz, 1uz] = dummy_value; + CHECK_EQ(sut[0uz, 1uz], dummy_value); + + CHECK_EQ(sut[0uz, 2uz], sut_row0[2uz]); + sut[0uz, 2uz] = dummy_value; + CHECK_EQ(sut[0uz, 2uz], dummy_value); + + CHECK_EQ(sut[1uz, 0uz], sut_row1[0uz]); + sut[1uz, 0uz] = dummy_value; + CHECK_EQ(sut[1uz, 0uz], dummy_value); + + CHECK_EQ(sut[1uz, 1uz], sut_row1[1uz]); + sut[1uz, 1uz] = dummy_value; + CHECK_EQ(sut[1uz, 1uz], dummy_value); + + CHECK_EQ(sut[1uz, 2uz], sut_row1[2uz]); + sut[1uz, 2uz] = dummy_value; + CHECK_EQ(sut[1uz, 2uz], dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const operator[](r, c) should return const element") { + const auto& const_sut = sut; + + CHECK_EQ(const_sut[0uz, 0uz], sut_row0[0uz]); + CHECK_EQ(const_sut[0uz, 1uz], sut_row0[1uz]); + CHECK_EQ(const_sut[0uz, 2uz], sut_row0[2uz]); + CHECK_EQ(const_sut[1uz, 0uz], sut_row1[0uz]); + CHECK_EQ(const_sut[1uz, 1uz], sut_row1[1uz]); + CHECK_EQ(const_sut[1uz, 2uz], sut_row1[2uz]); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r) should return row at given index") { + auto r0 = sut.at(0uz); + CHECK(std::ranges::equal(r0, data[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut.at(0uz).front(), dummy_value); + + auto r1 = sut.at(1uz); + CHECK(std::ranges::equal(r1, data[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut.at(1uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.at(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should return const row at given index") { + const auto& const_sut = sut; + + auto r0 = const_sut.at(0uz); + CHECK(std::ranges::equal(r0, data[0uz])); + + auto r1 = const_sut.at(1uz); + CHECK(std::ranges::equal(r1, data[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should throw for out of range index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "at(r, r) should return element at given row and position" +) { + CHECK_EQ(sut.at(0uz, 0uz), sut_row0[0uz]); + sut.at(0uz, 0uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 0uz), dummy_value); + + CHECK_EQ(sut.at(0uz, 1uz), sut_row0[1uz]); + sut.at(0uz, 1uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 1uz), dummy_value); + + CHECK_EQ(sut.at(0uz, 2uz), sut_row0[2uz]); + sut.at(0uz, 2uz) = dummy_value; + CHECK_EQ(sut.at(0uz, 2uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 0uz), sut_row1[0uz]); + sut.at(1uz, 0uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 0uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 1uz), sut_row1[1uz]); + sut.at(1uz, 1uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 1uz), dummy_value); + + CHECK_EQ(sut.at(1uz, 2uz), sut_row1[2uz]); + sut.at(1uz, 2uz) = dummy_value; + CHECK_EQ(sut.at(1uz, 2uz), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r, c) should throw for invalid row index") { + CHECK_THROWS_AS(static_cast(sut.at(2uz, 0uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(10uz, 0uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r, c) should throw for invalid col index") { + CHECK_THROWS_AS(static_cast(sut.at(0uz, 3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.at(1uz, 10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should return const element") { + const auto& const_sut = sut; + + CHECK_EQ(const_sut.at(0uz, 0uz), sut_row0[0uz]); + CHECK_EQ(const_sut.at(0uz, 1uz), sut_row0[1uz]); + CHECK_EQ(const_sut.at(0uz, 2uz), sut_row0[2uz]); + CHECK_EQ(const_sut.at(1uz, 0uz), sut_row1[0uz]); + CHECK_EQ(const_sut.at(1uz, 1uz), sut_row1[1uz]); + CHECK_EQ(const_sut.at(1uz, 2uz), sut_row1[2uz]); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should throw for invalid row index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(2uz, 0uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(10uz, 0uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should throw for invalid col index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.at(0uz, 3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.at(1uz, 10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "rows() should return a view of all rows") { + auto n_rows = 0uz; + for (auto row : sut.rows()) { + CHECK(std::ranges::equal(row, data[n_rows])); + const auto orig_val = std::exchange(row.front(), dummy_value); + CHECK_EQ(sut[n_rows].front(), dummy_value); + row.front() = orig_val; // revert change + + n_rows++; + } + + CHECK_EQ(n_rows, sut_n_rows); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front() should return the first row") { + auto front_row = sut.front(); + CHECK(std::ranges::equal(front_row, data.front())); + + front_row.front() = dummy_value; + CHECK_EQ(sut.front().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front() should return const first row") { + const auto& const_sut = sut; + auto front_row = const_sut.front(); + CHECK(std::ranges::equal(front_row, data.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front(row) should return first element in row") { + CHECK_EQ(sut.front(0uz), sut_row0.front()); + sut.front(0uz) = dummy_value; + CHECK_EQ(sut[0uz].front(), dummy_value); + + CHECK_EQ(sut.front(1uz), sut_row1.front()); + sut.front(1uz) = dummy_value; + CHECK_EQ(sut[1uz].front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const front(row) should return const first element in row" +) { + const auto& const_sut = sut; + CHECK_EQ(const_sut.front(0uz), sut_row0.front()); + CHECK_EQ(const_sut.front(1uz), sut_row1.front()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row") { + auto back_row = sut.back(); + CHECK(std::ranges::equal(back_row, data.back())); + + back_row.front() = dummy_value; + CHECK_EQ(sut.back().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back() should return const last row") { + const auto& const_sut = sut; + auto back_row = const_sut.back(); + CHECK(std::ranges::equal(back_row, data.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back(row) should return last element in row") { + CHECK_EQ(sut.back(0uz), sut_row0.back()); + sut.back(0uz) = dummy_value; + CHECK_EQ(sut[0uz].back(), dummy_value); + + CHECK_EQ(sut.back(1uz), sut_row1.back()); + sut.back(1uz) = dummy_value; + CHECK_EQ(sut[1uz].back(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const back(row) should return const last element in row" +) { + const auto& const_sut = sut; + CHECK_EQ(const_sut.back(0uz), sut_row0.back()); + CHECK_EQ(const_sut.back(1uz), sut_row1.back()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const rows() should return a const view of all rows" +) { + const auto& const_sut = sut; + + auto n_rows = 0uz; + for (auto row : const_sut.rows()) { + CHECK(std::ranges::equal(row, data[n_rows])); + n_rows++; + } + + CHECK_EQ(n_rows, sut_n_rows); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_size() should return the total number of elements in the matrix" +) { + CHECK_EQ(sut.data_size(), 6uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "data_view() should return a span of all data") { + CHECK(std::ranges::equal(sut.data_view(), flat_data)); + sut.data_view().front() = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const data_view() should return a const span of all data" +) { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.data_view(), flat_data)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_storage() should return a mutable reference to the internal vector" +) { + auto& storage_ref = sut.data_storage(); + CHECK(std::ranges::equal(storage_ref, flat_data)); + CHECK_EQ(storage_ref.data(), sut.data_view().data()); + + // check mutability + storage_ref.front() = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "const data_storage() should return a const reference to the internal vector" +) { + const auto& const_sut = sut; + const auto& storage_ref = const_sut.data_storage(); + + CHECK(std::ranges::equal(storage_ref, flat_data)); + CHECK_EQ(storage_ref.data(), const_sut.data_view().data()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "data_ptr() should return a mutable raw pointer to the first element" +) { + auto* ptr = sut.data_ptr(); + + CHECK_EQ(ptr, sut.data_view().data()); + CHECK(std::equal(ptr, ptr + sut.data_size(), flat_data.begin())); + + // check mutability + *ptr = dummy_value; + CHECK_EQ(sut.data_view().front(), dummy_value); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, + "const data_ptr() should return a const raw pointer to the first element" +) { + const auto& const_sut = sut; + const auto* ptr = const_sut.data_ptr(); + + CHECK_EQ(ptr, const_sut.data_view().data()); + CHECK(std::equal(ptr, ptr + const_sut.data_size(), flat_data.begin())); +} + TEST_SUITE_END(); // test_flat_matrix } // namespace gl_testing From f9bd37c97d96a84638b071953b1ef8e8999d012d Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Sun, 29 Mar 2026 20:39:03 +0200 Subject: [PATCH 07/11] flat matrix row modifiers tests --- include/gl/types/flat_matrix.hpp | 104 ++++++++--------- tests/source/gl/test_flat_matrix.cpp | 162 +++++++++++++++++++++++++++ 2 files changed, 216 insertions(+), 50 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 453d7b8a..ad95327a 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -16,6 +16,10 @@ namespace gl { +// TODO: +// col accessors -> views +// transposition + template class flat_matrix { public: @@ -405,56 +409,6 @@ class flat_matrix { return this->_data.data(); } - // --- iterators --- - - [[nodiscard]] iterator begin() noexcept { - return iterator(this->_data.data(), this->_n_cols, 0uz); - } - - [[nodiscard]] iterator end() noexcept { - return iterator(this->_data.data(), this->_n_cols, this->_n_rows); - } - - [[nodiscard]] const_iterator begin() const noexcept { - return const_iterator(this->_data.data(), this->_n_cols, 0uz); - } - - [[nodiscard]] const_iterator end() const noexcept { - return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); - } - - [[nodiscard]] const_iterator cbegin() const noexcept { - return this->begin(); - } - - [[nodiscard]] const_iterator cend() const noexcept { - return this->end(); - } - - [[nodiscard]] reverse_iterator rbegin() noexcept { - return reverse_iterator(this->end()); - } - - [[nodiscard]] reverse_iterator rend() noexcept { - return reverse_iterator(this->begin()); - } - - [[nodiscard]] const_reverse_iterator rbegin() const noexcept { - return const_reverse_iterator(this->end()); - } - - [[nodiscard]] const_reverse_iterator rend() const noexcept { - return const_reverse_iterator(this->begin()); - } - - [[nodiscard]] const_reverse_iterator crbegin() const noexcept { - return this->rbegin(); - } - - [[nodiscard]] const_reverse_iterator crend() const noexcept { - return this->rend(); - } - // --- modifiers (rows) --- template @@ -723,6 +677,56 @@ class flat_matrix { --this->_n_cols; } + // --- iterators --- + + [[nodiscard]] iterator begin() noexcept { + return iterator(this->_data.data(), this->_n_cols, 0uz); + } + + [[nodiscard]] iterator end() noexcept { + return iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + [[nodiscard]] const_iterator begin() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, 0uz); + } + + [[nodiscard]] const_iterator end() const noexcept { + return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); + } + + [[nodiscard]] const_iterator cbegin() const noexcept { + return this->begin(); + } + + [[nodiscard]] const_iterator cend() const noexcept { + return this->end(); + } + + [[nodiscard]] reverse_iterator rbegin() noexcept { + return reverse_iterator(this->end()); + } + + [[nodiscard]] reverse_iterator rend() noexcept { + return reverse_iterator(this->begin()); + } + + [[nodiscard]] const_reverse_iterator rbegin() const noexcept { + return const_reverse_iterator(this->end()); + } + + [[nodiscard]] const_reverse_iterator rend() const noexcept { + return const_reverse_iterator(this->begin()); + } + + [[nodiscard]] const_reverse_iterator crbegin() const noexcept { + return this->rbegin(); + } + + [[nodiscard]] const_reverse_iterator crend() const noexcept { + return this->rend(); + } + private: void _check_row(size_type r) const { if (r >= this->_n_rows) { diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index cebd2147..e28c66eb 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -812,6 +812,168 @@ TEST_CASE_FIXTURE( CHECK(std::equal(ptr, ptr + const_sut.data_size(), flat_data.begin())); } +struct test_flat_matrix_row_modifiers { + using sut_type = gl::flat_matrix; + + sut_type sut; + std::vector sut_row0{1, 2, 3}; + std::vector sut_row1{4, 5, 6}; + std::vector sut_row2{7, 8, 9}; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "push_row with span should add new row") { + sut.push_row(std::span{sut_row0}); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "push_row with vector should add new row") { + sut.push_row(sut_row0); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "push_row with initializer list should add new row" +) { + sut.push_row({1, 2, 3}); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "multiple push_row calls should add multiple rows" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row should remove last row") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + REQUIRE(std::ranges::equal(sut.back(), sut_row1)); + + sut.pop_row(); + + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size()); + CHECK(std::ranges::equal(sut.back(), sut_row0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row on empty container should do nothing") { + sut.pop_row(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "pop_row should remove all rows sequentially") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut.back(), sut_row2)); + + sut.pop_row(); + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size()); + CHECK(std::ranges::equal(sut.back(), sut_row1)); + + sut.pop_row(); + CHECK_EQ(sut.n_rows(), 1uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size()); + CHECK(std::ranges::equal(sut.back(), sut_row0)); + + sut.pop_row(); + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "insert_row should add row at given position") { + sut.push_row(sut_row0); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row2)); + + sut.insert_row(1uz, sut_row1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "insert_row should throw for an invalid position" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.insert_row(3uz, sut_row2), std::out_of_range); + CHECK_THROWS_AS(sut.insert_row(10uz, sut_row2), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "erase_row should remove segment at given position" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + sut.push_row(sut_row2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row1.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row1)); + CHECK(std::ranges::equal(sut[2uz], sut_row2)); + + sut.erase_row(1uz); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_row0.size() + sut_row2.size()); + CHECK(std::ranges::equal(sut[0uz], sut_row0)); + CHECK(std::ranges::equal(sut[1uz], sut_row2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "erase_row should throw for an invalid position") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.erase_row(2uz), std::out_of_range); + CHECK_THROWS_AS(sut.erase_row(10uz), std::out_of_range); +} + +// TODO: col modifiers, iterators, more (when added) + TEST_SUITE_END(); // test_flat_matrix } // namespace gl_testing From 99af437fc595d149268cc2b21abe5b656be824c4 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 30 Mar 2026 19:40:01 +0200 Subject: [PATCH 08/11] column modifiers tests + ext accessors tests --- include/gl/types/flat_matrix.hpp | 74 +++++- tests/source/gl/test_flat_matrix.cpp | 381 +++++++++++++++++++++++---- 2 files changed, 391 insertions(+), 64 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index ad95327a..61105c8c 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -355,20 +355,58 @@ class flat_matrix { return (*this)[this->_n_rows - 1uz]; } - [[nodiscard]] reference front(size_type r) noexcept { - return (*this)[r, 0uz]; + // TODO: add tests + + [[nodiscard]] row_type front_row() noexcept { + return this->front(); + } + + [[nodiscard]] const_row_type front_row() const noexcept { + return this->front(); + } + + [[nodiscard]] row_type back_row() noexcept { + return this->back(); + } + + [[nodiscard]] const_row_type back_row() const noexcept { + return this->back(); } - [[nodiscard]] const_reference front(size_type r) const noexcept { - return (*this)[r, 0uz]; + [[nodiscard]] auto front_col() noexcept { + return this->_col_impl(0uz); } - [[nodiscard]] reference back(size_type r) noexcept { - return (*this)[r, this->_n_cols - 1uz]; + [[nodiscard]] auto front_col() const noexcept { + return this->_col_impl(0uz); } - [[nodiscard]] const_reference back(size_type r) const noexcept { - return (*this)[r, this->_n_cols - 1uz]; + [[nodiscard]] auto back_col() noexcept { + return this->_col_impl(this->_n_cols - 1uz); + } + + [[nodiscard]] auto back_col() const noexcept { + return this->_col_impl(this->_n_cols - 1uz); + } + + // TODO: end + + [[nodiscard]] row_type row(size_type r) { + return this->at(r); + } + + [[nodiscard]] const_row_type row(size_type r) const { + return this->at(r); + } + + [[nodiscard]] auto col(size_type c) { + this->_check_col(c); + return this->_col_impl(c); + } + + [[nodiscard]] auto col(size_type c) const { + this->_check_col(c); + return this->_col_impl(c); } [[nodiscard]] auto rows() noexcept { @@ -381,6 +419,18 @@ class flat_matrix { | std::views::transform([this](size_type i) -> const_row_type { return (*this)[i]; }); } + [[nodiscard]] auto cols() noexcept { + return std::views::iota(size_type{0}, this->_n_cols) + | std::views::transform([this](size_type c) { return this->_col_impl(c); }); + } + + [[nodiscard]] auto cols() const noexcept { + return std::views::iota(size_type{0}, this->_n_cols) + | std::views::transform([this](size_type c) { return this->_col_impl(c); }); + } + + // --- accessors (data) --- + [[nodiscard]] size_type data_size() const noexcept { return this->_data.size(); } @@ -748,6 +798,14 @@ class flat_matrix { } } + [[nodiscard]] auto _col_impl(size_type c) noexcept { + return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); + } + + [[nodiscard]] auto _col_impl(size_type c) const noexcept { + return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); + } + size_type _n_rows{0uz}; size_type _n_cols{0uz}; std::vector _data; diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index e28c66eb..05ca5b47 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -485,7 +485,12 @@ struct test_flat_matrix_accessors { std::vector sut_row0{1, 2, 3}; std::vector sut_row1{4, 5, 6}; - std::vector> data{sut_row0, sut_row1}; + std::vector sut_col0{1, 4}; + std::vector sut_col1{2, 5}; + std::vector sut_col2{3, 6}; + + std::vector> sut_rows{sut_row0, sut_row1}; // rename to sut_rows + std::vector> sut_cols{sut_col0, sut_col1, sut_col2}; std::vector flat_data{1, 2, 3, 4, 5, 6}; std::size_t sut_n_rows = 2uz; @@ -496,12 +501,12 @@ struct test_flat_matrix_accessors { TEST_CASE_FIXTURE(test_flat_matrix_accessors, "operator[r] should return row at given index") { auto r0 = sut[0uz]; - CHECK(std::ranges::equal(r0, data[0uz])); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); r0.front() = dummy_value; CHECK_EQ(sut[0uz].front(), dummy_value); auto r1 = sut[1uz]; - CHECK(std::ranges::equal(r1, data[1uz])); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); r1.front() = dummy_value; CHECK_EQ(sut[1uz].front(), dummy_value); } @@ -512,10 +517,10 @@ TEST_CASE_FIXTURE( const auto& const_sut = sut; auto r0 = const_sut[0uz]; - CHECK(std::ranges::equal(r0, data[0uz])); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); auto r1 = const_sut[1uz]; - CHECK(std::ranges::equal(r1, data[1uz])); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); } TEST_CASE_FIXTURE( @@ -559,12 +564,12 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const operator[](r, c) should ret TEST_CASE_FIXTURE(test_flat_matrix_accessors, "at(r) should return row at given index") { auto r0 = sut.at(0uz); - CHECK(std::ranges::equal(r0, data[0uz])); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); r0.front() = dummy_value; CHECK_EQ(sut.at(0uz).front(), dummy_value); auto r1 = sut.at(1uz); - CHECK(std::ranges::equal(r1, data[1uz])); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); r1.front() = dummy_value; CHECK_EQ(sut.at(1uz).front(), dummy_value); } @@ -578,10 +583,10 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should return const r const auto& const_sut = sut; auto r0 = const_sut.at(0uz); - CHECK(std::ranges::equal(r0, data[0uz])); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); auto r1 = const_sut.at(1uz); - CHECK(std::ranges::equal(r1, data[1uz])); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); } TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r) should throw for out of range index") { @@ -651,23 +656,9 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const at(r, c) should throw for i CHECK_THROWS_AS(static_cast(const_sut.at(1uz, 10uz)), std::out_of_range); } -TEST_CASE_FIXTURE(test_flat_matrix_accessors, "rows() should return a view of all rows") { - auto n_rows = 0uz; - for (auto row : sut.rows()) { - CHECK(std::ranges::equal(row, data[n_rows])); - const auto orig_val = std::exchange(row.front(), dummy_value); - CHECK_EQ(sut[n_rows].front(), dummy_value); - row.front() = orig_val; // revert change - - n_rows++; - } - - CHECK_EQ(n_rows, sut_n_rows); -} - TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front() should return the first row") { auto front_row = sut.front(); - CHECK(std::ranges::equal(front_row, data.front())); + CHECK(std::ranges::equal(front_row, sut_rows.front())); front_row.front() = dummy_value; CHECK_EQ(sut.front().front(), dummy_value); @@ -676,57 +667,113 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front() should return the first r TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front() should return const first row") { const auto& const_sut = sut; auto front_row = const_sut.front(); - CHECK(std::ranges::equal(front_row, data.front())); + CHECK(std::ranges::equal(front_row, sut_rows.front())); } -TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front(row) should return first element in row") { - CHECK_EQ(sut.front(0uz), sut_row0.front()); - sut.front(0uz) = dummy_value; - CHECK_EQ(sut[0uz].front(), dummy_value); +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row") { + auto back_row = sut.back(); + CHECK(std::ranges::equal(back_row, sut_rows.back())); - CHECK_EQ(sut.front(1uz), sut_row1.front()); - sut.front(1uz) = dummy_value; - CHECK_EQ(sut[1uz].front(), dummy_value); + back_row.front() = dummy_value; + CHECK_EQ(sut.back().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back() should return const last row") { + const auto& const_sut = sut; + auto back_row = const_sut.back(); + CHECK(std::ranges::equal(back_row, sut_rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "row(r) should return row at given index") { + auto r0 = sut.row(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); + r0.front() = dummy_value; + CHECK_EQ(sut.row(0uz).front(), dummy_value); + + auto r1 = sut.row(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); + r1.front() = dummy_value; + CHECK_EQ(sut.row(1uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "row(r) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.row(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.row(10uz)), std::out_of_range); } TEST_CASE_FIXTURE( - test_flat_matrix_accessors, "const front(row) should return const first element in row" + test_flat_matrix_accessors, "const row(r) should return const row at given index" ) { const auto& const_sut = sut; - CHECK_EQ(const_sut.front(0uz), sut_row0.front()); - CHECK_EQ(const_sut.front(1uz), sut_row1.front()); -} -TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row") { - auto back_row = sut.back(); - CHECK(std::ranges::equal(back_row, data.back())); + auto r0 = const_sut.row(0uz); + CHECK(std::ranges::equal(r0, sut_rows[0uz])); - back_row.front() = dummy_value; - CHECK_EQ(sut.back().front(), dummy_value); + auto r1 = const_sut.row(1uz); + CHECK(std::ranges::equal(r1, sut_rows[1uz])); } -TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back() should return const last row") { +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const row(r) should throw for out of range index") { const auto& const_sut = sut; - auto back_row = const_sut.back(); - CHECK(std::ranges::equal(back_row, data.back())); + CHECK_THROWS_AS(static_cast(const_sut.row(2uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.row(10uz)), std::out_of_range); } -TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back(row) should return last element in row") { - CHECK_EQ(sut.back(0uz), sut_row0.back()); - sut.back(0uz) = dummy_value; - CHECK_EQ(sut[0uz].back(), dummy_value); +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "col(c) should return column at given index") { + auto c0 = sut.col(0uz); + CHECK(std::ranges::equal(c0, sut_col0)); + c0.front() = dummy_value; + CHECK_EQ(sut.col(0uz).front(), dummy_value); + + auto c1 = sut.col(1uz); + CHECK(std::ranges::equal(c1, sut_col1)); + c1.front() = dummy_value; + CHECK_EQ(sut.col(1uz).front(), dummy_value); - CHECK_EQ(sut.back(1uz), sut_row1.back()); - sut.back(1uz) = dummy_value; - CHECK_EQ(sut[1uz].back(), dummy_value); + auto c2 = sut.col(2uz); + CHECK(std::ranges::equal(c2, sut_col2)); + c2.front() = dummy_value; + CHECK_EQ(sut.col(2uz).front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "col(c) should throw for out of range index") { + CHECK_THROWS_AS(static_cast(sut.col(3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(sut.col(10uz)), std::out_of_range); } TEST_CASE_FIXTURE( - test_flat_matrix_accessors, "const back(row) should return const last element in row" + test_flat_matrix_accessors, "const col(c) should return const column at given index" ) { const auto& const_sut = sut; - CHECK_EQ(const_sut.back(0uz), sut_row0.back()); - CHECK_EQ(const_sut.back(1uz), sut_row1.back()); + + auto c0 = const_sut.col(0uz); + CHECK(std::ranges::equal(c0, sut_col0)); + + auto c1 = const_sut.col(1uz); + CHECK(std::ranges::equal(c1, sut_col1)); + + auto c2 = const_sut.col(2uz); + CHECK(std::ranges::equal(c2, sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const col(c) should throw for out of range index") { + const auto& const_sut = sut; + CHECK_THROWS_AS(static_cast(const_sut.col(3uz)), std::out_of_range); + CHECK_THROWS_AS(static_cast(const_sut.col(10uz)), std::out_of_range); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "rows() should return a view of all rows") { + auto n_rows = 0uz; + for (auto row : sut.rows()) { + CHECK(std::ranges::equal(row, sut_rows[n_rows])); + const auto orig_val = std::exchange(row.front(), dummy_value); + CHECK_EQ(sut[n_rows].front(), dummy_value); + row.front() = orig_val; // revert change + + n_rows++; + } + + CHECK_EQ(n_rows, sut_n_rows); } TEST_CASE_FIXTURE( @@ -736,13 +783,41 @@ TEST_CASE_FIXTURE( auto n_rows = 0uz; for (auto row : const_sut.rows()) { - CHECK(std::ranges::equal(row, data[n_rows])); + CHECK(std::ranges::equal(row, sut_rows[n_rows])); n_rows++; } CHECK_EQ(n_rows, sut_n_rows); } +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "cols() should return a view of all columns") { + auto n_cols = 0uz; + for (auto col : sut.cols()) { + CHECK(std::ranges::equal(col, sut_cols[n_cols])); + const auto orig_val = std::exchange(col.front(), dummy_value); + CHECK_EQ(sut.col(n_cols).front(), dummy_value); + col.front() = orig_val; // revert change + + n_cols++; + } + + CHECK_EQ(n_cols, sut_n_cols); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_accessors, "const cols() should return a const view of all columns" +) { + const auto& const_sut = sut; + + auto n_cols = 0uz; + for (auto col : const_sut.cols()) { + CHECK(std::ranges::equal(col, sut_cols[n_cols])); + n_cols++; + } + + CHECK_EQ(n_cols, sut_n_cols); +} + TEST_CASE_FIXTURE( test_flat_matrix_accessors, "data_size() should return the total number of elements in the matrix" @@ -850,6 +925,15 @@ TEST_CASE_FIXTURE( CHECK(std::ranges::equal(sut[0uz], sut_row0)); } +TEST_CASE_FIXTURE( + test_flat_matrix_row_modifiers, "push_row should throw for a not matching row size" +) { + sut.push_row(sut_row0); + sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.push_row({11, 22}), std::invalid_argument); +} + TEST_CASE_FIXTURE( test_flat_matrix_row_modifiers, "multiple push_row calls should add multiple rows" ) { @@ -942,10 +1026,17 @@ TEST_CASE_FIXTURE( } TEST_CASE_FIXTURE( - test_flat_matrix_row_modifiers, "erase_row should remove segment at given position" + test_flat_matrix_row_modifiers, "insert_row should throw for a not matching row size" ) { sut.push_row(sut_row0); sut.push_row(sut_row1); + + CHECK_THROWS_AS(sut.insert_row(1uz, {11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "erase_row should remove row at given position") { + sut.push_row(sut_row0); + sut.push_row(sut_row1); sut.push_row(sut_row2); CHECK_EQ(sut.n_rows(), 3uz); @@ -972,6 +1063,184 @@ TEST_CASE_FIXTURE(test_flat_matrix_row_modifiers, "erase_row should throw for an CHECK_THROWS_AS(sut.erase_row(10uz), std::out_of_range); } +struct test_flat_matrix_col_modifiers { + using sut_type = gl::flat_matrix; + + sut_type sut; + std::vector sut_col0{1, 2, 3}; + std::vector sut_col1{4, 5, 6}; + std::vector sut_col2{7, 8, 9}; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "push_col with span should add new column") { + sut.push_col(std::span{sut_col0}); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "push_col with vector should add new column") { + sut.push_col(sut_col0); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "push_col with initializer list should add new column" +) { + sut.push_col({1, 2, 3}); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), 3uz); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "push_col should throw for a not matching col size" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.push_col({11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "multiple push_col calls should add multiple columns" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.size(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col should remove last column") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + REQUIRE(std::ranges::equal(sut.back_col(), sut_col1)); + + sut.pop_col(); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), sut_col0.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col0)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col on empty container should do nothing") { + sut.pop_col(); + CHECK(sut.empty()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "pop_col should remove all columns sequentially") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col2)); + + sut.pop_col(); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col1)); + + sut.pop_col(); + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 1uz); + CHECK_EQ(sut.data_size(), sut_col0.size()); + CHECK(std::ranges::equal(sut.back_col(), sut_col0)); + + sut.pop_col(); + CHECK(sut.empty()); + CHECK_EQ(sut.n_rows(), 0uz); + CHECK_EQ(sut.n_cols(), 0uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "insert_col should add column at given position") { + sut.push_col(sut_col0); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col2)); + + sut.insert_col(1uz, sut_col1); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "insert_col should throw for an invalid position" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.insert_row(4uz, sut_col2), std::out_of_range); + CHECK_THROWS_AS(sut.insert_row(10uz, sut_col2), std::out_of_range); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "insert_col should throw for a not matching col size" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.insert_col(1uz, {11, 22}), std::invalid_argument); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_col_modifiers, "erase_col should remove column at given position" +) { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + sut.push_col(sut_col2); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col1.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col1)); + CHECK(std::ranges::equal(sut.col(2uz), sut_col2)); + + sut.erase_col(1uz); + + CHECK_EQ(sut.n_rows(), 3uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), sut_col0.size() + sut_col2.size()); + CHECK(std::ranges::equal(sut.col(0uz), sut_col0)); + CHECK(std::ranges::equal(sut.col(1uz), sut_col2)); +} + +TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "erase_col should throw for an invalid position") { + sut.push_col(sut_col0); + sut.push_col(sut_col1); + + CHECK_THROWS_AS(sut.erase_col(2uz), std::out_of_range); + CHECK_THROWS_AS(sut.erase_col(10uz), std::out_of_range); +} + // TODO: col modifiers, iterators, more (when added) TEST_SUITE_END(); // test_flat_matrix From d820f95f317a9e0f3f276f53779493fa55717a22 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 30 Mar 2026 19:43:51 +0200 Subject: [PATCH 09/11] front/back_row/col accessors tests --- include/gl/types/flat_matrix.hpp | 5 --- tests/source/gl/test_flat_matrix.cpp | 56 ++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 61105c8c..95694608 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -17,7 +17,6 @@ namespace gl { // TODO: -// col accessors -> views // transposition template @@ -355,8 +354,6 @@ class flat_matrix { return (*this)[this->_n_rows - 1uz]; } - // TODO: add tests - [[nodiscard]] row_type front_row() noexcept { return this->front(); } @@ -389,8 +386,6 @@ class flat_matrix { return this->_col_impl(this->_n_cols - 1uz); } - // TODO: end - [[nodiscard]] row_type row(size_type r) { return this->at(r); } diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index 05ca5b47..aeece0ae 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -666,8 +666,7 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front() should return the first r TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front() should return const first row") { const auto& const_sut = sut; - auto front_row = const_sut.front(); - CHECK(std::ranges::equal(front_row, sut_rows.front())); + CHECK(std::ranges::equal(const_sut.front(), sut_rows.front())); } TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row") { @@ -680,8 +679,59 @@ TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back() should return the last row TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back() should return const last row") { const auto& const_sut = sut; - auto back_row = const_sut.back(); + CHECK(std::ranges::equal(const_sut.back(), sut_rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front_row() should return the first row") { + auto front_row = sut.front_row(); + CHECK(std::ranges::equal(front_row, sut_rows.front())); + + front_row.front() = dummy_value; + CHECK_EQ(sut.front_row().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front_row() should return const first row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.front_row(), sut_rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back_row() should return the last row") { + auto back_row = sut.back_row(); CHECK(std::ranges::equal(back_row, sut_rows.back())); + + back_row.front() = dummy_value; + CHECK_EQ(sut.back_row().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back_row() should return const last row") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.back_row(), sut_rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "front_col() should return the first column") { + auto front_col = sut.front_col(); + CHECK(std::ranges::equal(front_col, sut_cols.front())); + + front_col.front() = dummy_value; + CHECK_EQ(sut.front_col().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const front_col() should return const first column") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.front_col(), sut_cols.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "back_col() should return the last column") { + auto back_col = sut.back_col(); + CHECK(std::ranges::equal(back_col, sut_cols.back())); + + back_col.front() = dummy_value; + CHECK_EQ(sut.back_col().front(), dummy_value); +} + +TEST_CASE_FIXTURE(test_flat_matrix_accessors, "const back_col() should return const last column") { + const auto& const_sut = sut; + CHECK(std::ranges::equal(const_sut.back_col(), sut_cols.back())); } TEST_CASE_FIXTURE(test_flat_matrix_accessors, "row(r) should return row at given index") { From c61aad9d50645c0f332b91d9f3b0f909a2d489a3 Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 30 Mar 2026 19:52:23 +0200 Subject: [PATCH 10/11] remaining tests --- include/gl/types/flat_matrix.hpp | 13 +- tests/source/gl/test_flat_matrix.cpp | 359 ++++++++++++++++++++++++++- 2 files changed, 368 insertions(+), 4 deletions(-) diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 95694608..550016be 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -16,9 +16,6 @@ namespace gl { -// TODO: -// transposition - template class flat_matrix { public: @@ -772,6 +769,16 @@ class flat_matrix { return this->rend(); } + // --- transformations --- + + [[nodiscard]] flat_matrix transpose() const { + flat_matrix result(this->_n_cols, this->_n_rows); + for (size_type r = 0uz; r < this->_n_rows; ++r) + for (size_type c = 0uz; c < this->_n_cols; ++c) + result[c, r] = (*this)[r, c]; + return result; + } + private: void _check_row(size_type r) const { if (r >= this->_n_rows) { diff --git a/tests/source/gl/test_flat_matrix.cpp b/tests/source/gl/test_flat_matrix.cpp index aeece0ae..1696d3d4 100644 --- a/tests/source/gl/test_flat_matrix.cpp +++ b/tests/source/gl/test_flat_matrix.cpp @@ -1291,7 +1291,364 @@ TEST_CASE_FIXTURE(test_flat_matrix_col_modifiers, "erase_col should throw for an CHECK_THROWS_AS(sut.erase_col(10uz), std::out_of_range); } -// TODO: col modifiers, iterators, more (when added) +struct test_flat_matrix_complex_operations { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "interleaved row and col operations should work correctly" +) { + sut_type sut; + + sut.push_row({1, 2}); + sut.push_col({3}); + sut.insert_row(0uz, {10, 20, 30}); + sut.insert_col(1uz, {15, 25}); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 4uz); + CHECK_EQ(sut.data_size(), 8uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{10, 15, 20, 30})); + CHECK(std::ranges::equal(sut[1uz], std::vector{1, 25, 2, 3})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "clearing and refilling should work correctly" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6} + }; + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 3uz); + CHECK_EQ(sut.data_size(), 6uz); + + sut.clear(); + + CHECK(sut.empty()); + + sut.push_row({9, 8}); + sut.push_row({7, 6}); + + CHECK_EQ(sut.n_rows(), 2uz); + CHECK_EQ(sut.n_cols(), 2uz); + CHECK_EQ(sut.data_size(), 4uz); + CHECK(std::ranges::equal(sut[0uz], std::vector{9, 8})); + CHECK(std::ranges::equal(sut[1uz], std::vector{7, 6})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_complex_operations, "large flat_matrix operations should maintain integrity" +) { + sut_type sut; + + for (int i = 0; i < 100; ++i) { + std::vector row; + for (int j = 0; j < 10; ++j) + row.push_back(i * 10 + j); + sut.push_row(row); + } + + CHECK_EQ(sut.n_rows(), 100uz); + CHECK_EQ(sut.n_cols(), 10uz); + CHECK_EQ(sut.data_size(), 1000uz); + for (int i = 0; i < 100; ++i) { + auto row = sut[i]; + for (int j = 0; j < 10; ++j) + CHECK_EQ(row[j], i * 10 + j); + } +} + +struct test_flat_matrix_iterators { + using sut_type = gl::flat_matrix; + + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + std::vector> rows{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; +}; + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "begin() should return iterator to first row") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "end() should return iterator past last row") { + auto it_begin = sut.begin(); + auto it_end = sut.end(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "const begin() should return const iterator to first row" +) { + const auto& const_sut = sut; + auto it = const_sut.begin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "const end() should return const iterator past last row" +) { + const auto& const_sut = sut; + auto it_begin = const_sut.begin(); + auto it_end = const_sut.end(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "cbegin() should return const iterator") { + auto it = sut.cbegin(); + CHECK(std::ranges::equal(*it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "cend() should return const iterator") { + auto it_begin = sut.cbegin(); + auto it_end = sut.cend(); + CHECK_EQ(it_end - it_begin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "non-const iterator should convert to const iterator implicitly" +) { + auto non_const_it = sut.begin(); + typename sut_type::const_iterator const_it = non_const_it; + CHECK(std::ranges::equal(*const_it, rows.front())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "dereferencing iterator should return row") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + ++it; + CHECK(std::ranges::equal(*it, rows[1uz])); + + ++it; + CHECK(std::ranges::equal(*it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator[] should access row at offset") { + auto it = sut.begin(); + CHECK(std::ranges::equal(it[0uz], rows[0uz])); + CHECK(std::ranges::equal(it[1uz], rows[1uz])); + CHECK(std::ranges::equal(it[2uz], rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "pre-increment should advance iterator") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + auto& ret = ++it; + CHECK(std::ranges::equal(*ret, rows[1uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "post-increment should return old iterator") { + auto it = sut.begin(); + CHECK(std::ranges::equal(*it, rows[0uz])); + + auto old_it = it++; + CHECK(std::ranges::equal(*old_it, rows[0uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "pre-decrement should move iterator backward") { + auto it = sut.end(); + --it; + CHECK(std::ranges::equal(*it, rows[2uz])); + + auto& ret = --it; + CHECK(std::ranges::equal(*ret, rows[1uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "post-decrement should return old iterator") { + auto it = sut.end(); + --it; + CHECK(std::ranges::equal(*it, rows[2uz])); + + auto old_it = it--; + CHECK(std::ranges::equal(*old_it, rows[2uz])); + CHECK(std::ranges::equal(*it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator+= should advance iterator") { + auto it = sut.begin(); + it += 2; + CHECK(std::ranges::equal(*it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator-= should move iterator backward") { + auto it = sut.end(); + it -= 1; + CHECK(std::ranges::equal(*it, rows[2uz])); + + it -= 2; + CHECK(std::ranges::equal(*it, rows[0uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator+ should create new iterator") { + auto it = sut.begin(); + auto new_it = it + 1; + + CHECK(std::ranges::equal(*it, rows[0uz])); + CHECK(std::ranges::equal(*new_it, rows[1uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "reverse operator+ should create new iterator") { + auto it = sut.begin(); + auto new_it = 2 + it; + + CHECK(std::ranges::equal(*it, rows[0uz])); + CHECK(std::ranges::equal(*new_it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator- should create new iterator") { + auto it = sut.end(); + auto new_it = it - 1; + + CHECK(std::ranges::equal(*new_it, rows[2uz])); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator- with two iterators should give distance") { + auto it1 = sut.begin(); + auto it2 = sut.end(); + + CHECK_EQ(it2 - it1, sut.n_rows()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator== should compare iterators") { + auto it1 = sut.begin(); + auto it2 = sut.begin(); + auto it3 = sut.begin() + 1; + + CHECK_EQ(it1, it2); + CHECK_NE(it1, it3); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "operator<=> should compare iterators") { + auto it1 = sut.begin(); + auto it2 = sut.begin() + 1; + auto it3 = sut.begin() + 2; + + CHECK_LT(it1, it2); + CHECK_LT(it2, it3); + CHECK_LE(it1, it2); + CHECK_LE(it1, it1); + CHECK_GT(it2, it1); + CHECK_GT(it3, it2); + CHECK_GE(it2, it1); + CHECK_GE(it2, it2); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "range-based for loop should iterate all rows") { + std::size_t idx = 0uz; + for (auto row : sut) + CHECK(std::ranges::equal(row, rows[idx++])); + CHECK_EQ(idx, 3uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "const range-based for loop should iterate all rows") { + const auto& const_sut = sut; + std::size_t idx = 0uz; + for (auto row : const_sut) + CHECK(std::ranges::equal(row, rows[idx++])); + CHECK_EQ(idx, 3uz); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "rbegin() should return reverse iterator") { + auto it = sut.rbegin(); + CHECK(std::ranges::equal(*it, rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "rend() should return reverse iterator past first") { + auto it_rbegin = sut.rbegin(); + auto it_rend = sut.rend(); + CHECK_EQ(it_rend - it_rbegin, rows.size()); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "crbegin() should return const reverse iterator") { + auto it = sut.crbegin(); + CHECK(std::ranges::equal(*it, rows.back())); +} + +TEST_CASE_FIXTURE(test_flat_matrix_iterators, "crend() should return const reverse iterator") { + auto it_rbegin = sut.crbegin(); + auto it_rend = sut.crend(); + CHECK_EQ(it_rend - it_rbegin, rows.size()); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_iterators, "reverse range-based for loop should iterate in reverse" +) { + std::size_t idx = 2uz; + for (auto it : std::ranges::reverse_view(sut)) { + CHECK(std::ranges::equal(it, rows[idx])); + if (idx > 0uz) + --idx; + } +} + +struct test_flat_matrix_transformations { + using sut_type = gl::flat_matrix; +}; + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose should correctly transpose a square matrix" +) { + sut_type sut{ + {1, 2, 3}, + {4, 5, 6}, + {7, 8, 9} + }; + + auto transposed = sut.transpose(); + + CHECK_EQ(transposed.n_rows(), 3uz); + CHECK_EQ(transposed.n_cols(), 3uz); + CHECK_EQ(transposed.data_size(), 9uz); + CHECK(std::ranges::equal(transposed[0uz], std::vector{1, 4, 7})); + CHECK(std::ranges::equal(transposed[1uz], std::vector{2, 5, 8})); + CHECK(std::ranges::equal(transposed[2uz], std::vector{3, 6, 9})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose should correctly transpose a rectangular matrix" +) { + sut_type sut{ + {1, 2, 3, 4}, + {5, 6, 7, 8} + }; + + auto transposed = sut.transpose(); + + CHECK_EQ(transposed.n_rows(), 4uz); + CHECK_EQ(transposed.n_cols(), 2uz); + CHECK_EQ(transposed.data_size(), 8uz); + CHECK(std::ranges::equal(transposed[0uz], std::vector{1, 5})); + CHECK(std::ranges::equal(transposed[1uz], std::vector{2, 6})); + CHECK(std::ranges::equal(transposed[2uz], std::vector{3, 7})); + CHECK(std::ranges::equal(transposed[3uz], std::vector{4, 8})); +} + +TEST_CASE_FIXTURE( + test_flat_matrix_transformations, "transpose on an empty matrix should return an empty matrix" +) { + sut_type sut; + + auto transposed = sut.transpose(); + + CHECK(transposed.empty()); + CHECK_EQ(transposed.n_rows(), 0uz); + CHECK_EQ(transposed.n_cols(), 0uz); + CHECK_EQ(transposed.data_size(), 0uz); +} TEST_SUITE_END(); // test_flat_matrix From d8b10360f3577f128241dfbffed04b84d3c26a1e Mon Sep 17 00:00:00 2001 From: SpectraL519 Date: Mon, 30 Mar 2026 20:38:36 +0200 Subject: [PATCH 11/11] flat matrix doc comments + removed reserve calls in push/insert operations on flat structures --- include/gl/types/flat_jagged_vector.hpp | 4 - include/gl/types/flat_matrix.hpp | 487 +++++++++++++++++++++++- 2 files changed, 482 insertions(+), 9 deletions(-) diff --git a/include/gl/types/flat_jagged_vector.hpp b/include/gl/types/flat_jagged_vector.hpp index 813d299f..4d2e1805 100644 --- a/include/gl/types/flat_jagged_vector.hpp +++ b/include/gl/types/flat_jagged_vector.hpp @@ -846,8 +846,6 @@ class flat_jagged_vector { requires std::convertible_to, value_type> void push_back(R&& r) { this->_ensure_offset_capacity(); - if constexpr (std::ranges::sized_range) - this->_data.reserve(this->_data.size() + std::ranges::size(r)); if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); @@ -908,8 +906,6 @@ class flat_jagged_vector { const auto old_size = this->_data.size(); this->_ensure_offset_capacity(); - if constexpr (std::ranges::sized_range) - this->_data.reserve(this->_data.size() + std::ranges::size(r)); if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); diff --git a/include/gl/types/flat_matrix.hpp b/include/gl/types/flat_matrix.hpp index 550016be..f7a033b5 100644 --- a/include/gl/types/flat_matrix.hpp +++ b/include/gl/types/flat_matrix.hpp @@ -16,105 +16,187 @@ namespace gl { +/// @brief A flattened 2D matrix providing efficient storage and uniform access for a rectangular grid of elements. +/// +/// This container stores all elements in a single contiguous memory block (`_data`) of size `n_rows * n_cols` +/// using row-major ordering. Row accesses are contiguous in memory, while column accesses are resolved mathematically +/// via strided views. Both provide $O(1)$ random access and native compatibility with C++20/23 ranges. +/// +/// @tparam T A semiregular type to be stored in the matrix. Must be copy-constructible and assignable. +/// +/// @warning Iterator invalidation follows `std::vector` semantics: modifying the dimensions or structural +/// capacity of the matrix invalidates all iterators, pointers, and references to its elements. template class flat_matrix { public: + /// @brief Type of elements stored in the matrix using value_type = T; + /// @brief Unsigned integral type used for sizes and indices using size_type = std::size_t; + /// @brief Reference to an element using reference = value_type&; + /// @brief Const reference to an element using const_reference = const value_type&; + /// @brief Span type representing a non-owning uniform row of elements using row_type = std::span; + /// @brief Const span type representing a non-owning uniform const row of elements using const_row_type = std::span; // --- iterators --- + /// @brief Random access iterator over the rows of the `flat_matrix`. + /// + /// This iterator dereferences to a `row_type` (span of elements representing a single matrix row), + /// allowing efficient iteration and random access. It calculates the memory offsets mathematically + /// based on the column dimension. + /// + /// @tparam Const If `true`, produces const iterators; if `false`, produces mutable iterators. + /// @note Provides random access semantics: `O(1)` for all operations except construction. + /// @warning Invalidated when the `flat_matrix` structural dimensions are modified or memory is reallocated. template class row_iterator { using data_ptr_type = std::conditional_t; public: + /// @brief Satisfies random access iterator concept using iterator_concept = std::random_access_iterator_tag; + /// @brief Legacy iterator category (random access) using iterator_category = std::random_access_iterator_tag; + /// @brief Type of row this iterator dereferences to (span or const span) using value_type = std::conditional_t; + /// @brief Signed integral difference type using difference_type = std::ptrdiff_t; + /// @brief Pointer type (void because iterators dereference to spans) using pointer = void; + /// @brief Reference type (span of elements) using reference = value_type; + /// @brief Default constructor creates a null iterator row_iterator() = default; - row_iterator(data_ptr_type data_ptr, size_type row_size, size_type row_idx) noexcept - : _data_ptr(data_ptr), _row_size(row_size), _row_idx(row_idx) {} + /// @brief Constructs an iterator pointing to a specific row. + /// @param data_ptr Pointer to the underlying flat element data + /// @param n_cols The number of columns in the matrix + /// @param row_idx The index of the row this iterator currently points to + row_iterator(data_ptr_type data_ptr, size_type n_cols, size_type row_idx) noexcept + : _data_ptr(data_ptr), _row_size(n_cols), _row_idx(row_idx) {} + /// @brief Implicit conversion from mutable to const iterator + /// @return A const iterator pointing to the same row operator row_iterator() const noexcept requires(not Const) { return row_iterator(this->_data_ptr, this->_row_size, this->_row_idx); } + /// @brief Dereferences the iterator to the current row. + /// @return A span representing the row at the current position [[nodiscard]] reference operator*() const noexcept { return reference(this->_data_ptr + this->_row_idx * this->_row_size, this->_row_size); } + /// @brief Random access to a row at an offset from the current position. + /// @param n Offset (can be negative) + /// @return Row at offset n from the current position + /// @pre `0 <= current_position + n < n_rows()`; otherwise Undefined Behavior [[nodiscard]] reference operator[](difference_type n) const noexcept { return *(*this + n); } + /// @brief Pre-increment operator. + /// @return Reference to this iterator after advancing to the next row row_iterator& operator++() noexcept { ++this->_row_idx; return *this; } + /// @brief Post-increment operator. + /// @return A copy of this iterator before the increment row_iterator operator++(int) noexcept { auto tmp = *this; ++this->_row_idx; return tmp; } + /// @brief Pre-decrement operator. + /// @return Reference to this iterator after moving to the previous row row_iterator& operator--() noexcept { --this->_row_idx; return *this; } + /// @brief Post-decrement operator. + /// @return A copy of this iterator before the decrement row_iterator operator--(int) noexcept { auto tmp = *this; --this->_row_idx; return tmp; } + /// @brief Advances the iterator by n rows. + /// @param n Number of rows to advance (can be negative) + /// @return Reference to this iterator row_iterator& operator+=(difference_type n) noexcept { this->_row_idx += n; return *this; } + /// @brief Moves the iterator backward by n rows. + /// @param n Number of rows to move backward (can be negative) + /// @return Reference to this iterator row_iterator& operator-=(difference_type n) noexcept { this->_row_idx -= n; return *this; } + /// @brief Creates a new iterator advanced by n rows from the given iterator. + /// @param it Iterator to advance from + /// @param n Number of rows to advance + /// @return New iterator at the advanced position [[nodiscard]] friend row_iterator operator+(row_iterator it, difference_type n) noexcept { return it += n; } + /// @brief Creates a new iterator advanced by n rows (commutative form). + /// @param n Number of rows to advance + /// @param it Iterator to advance from + /// @return New iterator at the advanced position [[nodiscard]] friend row_iterator operator+(difference_type n, row_iterator it) noexcept { return it += n; } + /// @brief Creates a new iterator moved backward by n rows. + /// @param it Iterator to move backward from + /// @param n Number of rows to move backward + /// @return New iterator at the moved position [[nodiscard]] friend row_iterator operator-(row_iterator it, difference_type n) noexcept { return it -= n; } + /// @brief Computes the distance between two iterators. + /// @param lhs The later iterator + /// @param rhs The earlier iterator + /// @return Number of rows between the iterators; negative if lhs < rhs [[nodiscard]] friend difference_type operator-( const row_iterator& lhs, const row_iterator& rhs ) noexcept { return lhs._row_idx - rhs._row_idx; } + /// @brief Tests equality of two iterators. + /// @param lhs Left iterator + /// @param rhs Right iterator + /// @return `true` if both iterators point to the same row index [[nodiscard]] friend bool operator==( const row_iterator& lhs, const row_iterator& rhs ) noexcept { return lhs._row_idx == rhs._row_idx; } + /// @brief Three-way comparison of two iterators. + /// @param lhs Left iterator + /// @param rhs Right iterator + /// @return Comparison result indicating iterator ordering [[nodiscard]] friend auto operator<=>( const row_iterator& lhs, const row_iterator& rhs ) noexcept { @@ -127,23 +209,47 @@ class flat_matrix { size_type _row_idx{0uz}; }; + /// @brief Mutable random access iterator over rows using iterator = row_iterator; + /// @brief Const random access iterator over rows using const_iterator = row_iterator; + /// @brief Reverse mutable iterator over rows using reverse_iterator = std::reverse_iterator; + /// @brief Reverse const iterator over rows using const_reverse_iterator = std::reverse_iterator; // --- constructors and assignment --- + /// @brief Default constructor creates an empty `flat_matrix`. + /// @post `empty() == true`, `n_rows() == 0`, `n_cols() == 0`, `data_size() == 0` flat_matrix() = default; + /// @brief Copy constructor creates a deep copy of another `flat_matrix`. + /// @param other The `flat_matrix` to copy + /// @post `*this == other` flat_matrix(const flat_matrix&) = default; + + /// @brief Copy assignment creates a deep copy of another `flat_matrix`. + /// @param other The source `flat_matrix` + /// @return Reference to `*this` + /// @post `*this == other` flat_matrix& operator=(const flat_matrix&) = default; + /// @brief Move constructor transfers ownership of data from another `flat_matrix`. + /// @param other The source `flat_matrix` (left in an empty state) + /// @post `other.empty() == true`; all data is transferred to `*this` + /// @warning Invalidates all iterators, pointers, and references to `other`'s elements. flat_matrix(flat_matrix&& other) noexcept : _n_rows(std::exchange(other._n_rows, 0uz)), _n_cols(std::exchange(other._n_cols, 0uz)), _data(std::move(other._data)) {} + /// @brief Move assignment transfers ownership of data from another `flat_matrix`. + /// @param other The source `flat_matrix` + /// @return Reference to `*this` + /// @post `other.empty() == true`; all data from `other` is transferred to `*this` + /// @warning Invalidates all iterators, pointers, and references to this container's elements. + /// @note This operator safely handles self-assignment. flat_matrix& operator=(flat_matrix&& other) noexcept { if (this != &other) { this->_n_rows = std::exchange(other._n_rows, 0uz); @@ -153,11 +259,24 @@ class flat_matrix { return *this; } + /// @brief Destructor cleans up all managed memory. ~flat_matrix() = default; + /// @brief Constructs a `flat_matrix` with specified dimensions. + /// @param n_rows The number of rows + /// @param n_cols The number of columns + /// @param value The value to initialize all elements with (default constructed if omitted) + /// @post `n_rows() == n_rows`, `n_cols() == n_cols`, and elements equal `value` + /// @exception std::bad_alloc May throw if memory allocation fails flat_matrix(size_type n_rows, size_type n_cols, const value_type& value = value_type{}) : _n_rows(n_rows), _n_cols(n_cols), _data(n_rows * n_cols, value) {} + /// @brief Constructs a `flat_matrix` from an initializer list of rows. + /// @param ilist Initializer list of initializer lists, each representing a row + /// @post Dimensions are established based on the list geometry + /// @exception std::invalid_argument If the rows in the list do not have identical lengths + /// @exception std::bad_alloc May throw if memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after construction flat_matrix(std::initializer_list> ilist) { this->_n_rows = ilist.size(); if (this->_n_rows == 0uz) @@ -178,6 +297,17 @@ class flat_matrix { } } + /// @brief Constructs a `flat_matrix` from a 2D range of ranges. + /// + /// The matrix establishes its column count from the size of the first extracted row. + /// All subsequent rows must perfectly match this dimension. Provides a strong exception guarantee + /// if the source is an unsized pure `input_range` and fails validation mid-extraction. + /// + /// @tparam R A range type whose elements are input ranges of `value_type` + /// @param r The 2D range to initialize from + /// @post Dimensions match the structure of `r` + /// @exception std::invalid_argument If any extracted row size mismatches the first row's size + /// @exception std::bad_alloc May throw if memory allocation fails template requires std::ranges::input_range> and std::convertible_to< @@ -230,38 +360,73 @@ class flat_matrix { // --- comparison --- + /// @brief Tests equality of two `flat_matrix` instances. + /// @param lhs Left operand + /// @param rhs Right operand + /// @return `true` if dimensions and all elements match friend bool operator==(const flat_matrix&, const flat_matrix&) = default; // --- size and capacity --- + /// @brief Returns the number of rows in the matrix. + /// @return The count of rows + /// @note Required to idiomaticaly satisfy `std::ranges::sized_range`. [[nodiscard]] size_type size() const noexcept { return this->_n_rows; } + /// @brief Returns the number of rows in the matrix. + /// @return The count of rows [[nodiscard]] size_type n_rows() const noexcept { return this->_n_rows; } + /// @brief Returns the number of columns in the matrix. + /// @return The count of columns [[nodiscard]] size_type n_cols() const noexcept { return this->_n_cols; } + /// @brief Checks if the container is entirely empty. + /// @return `true` if `data_size() == 0`, `false` otherwise [[nodiscard]] bool empty() const noexcept { return this->_data.empty(); } + /// @brief Returns the current capacity for data elements. + /// @return The number of total elements that can be stored in `_data` without reallocation [[nodiscard]] size_type data_capacity() const noexcept { return this->_data.capacity(); } + /// @brief Reserves space for at least n total elements without changing the dimensions. + /// @param n The total number of matrix elements to reserve space for + /// @post `data_capacity() >= n` + /// @warning Invalidates all iterators and pointers to elements if reallocation occurs void reserve_data(size_type n) { this->_data.reserve(n); } + /// @brief Reduces capacity of the internal array to match the current data size. + /// @post `data_capacity() == data_size()` + /// @warning Invalidates all iterators, pointers, and references to elements if reallocation occures void shrink_to_fit() { this->_data.shrink_to_fit(); } + /// @brief Resizes the mathematical dimensions of the matrix. + /// + /// If the new dimensions require structural changes (e.g. changing the number of columns), + /// the mathematical grid is rebuilt and existing items are relocated to their new coordinate slots. + /// + /// @param new_rows The new number of rows + /// @param new_cols The new number of columns + /// @param value The value to initialize any newly exposed slots with + /// @post `n_rows() == new_rows` and `n_cols() == new_cols` + /// @warning Invalidates all iterators, pointers, and references. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are the new dimensions, due to remapping + /// elements in 2D space. If only the row count changes, it is $O(K)$ where $K$ is the number of + /// inserted or removed trailing elements. void resize(size_type new_rows, size_type new_cols, const value_type& value = value_type{}) { if (new_rows == this->_n_rows and new_cols == this->_n_cols) return; @@ -285,6 +450,9 @@ class flat_matrix { this->_n_cols = new_cols; } + /// @brief Removes all dimensions and elements, leaving the matrix empty. + /// @post `n_rows() == 0`, `n_cols() == 0`, `data_size() == 0` + /// @warning Invalidates all iterators, pointers, and references to elements void clear() { this->_data.clear(); this->_n_rows = 0uz; @@ -293,129 +461,242 @@ class flat_matrix { // --- accessors --- + /// @brief Computes the underlying flattened 1D index for a 2D coordinate. + /// @param r The row index + /// @param c The column index + /// @return The 1D index mapping for `_data` + /// @note Provides $O(1)$ constant time lookup calculation [[nodiscard]] constexpr size_type index(size_type r, size_type c) const noexcept { return r * this->_n_cols + c; } + /// @brief Returns the row at the given index without bounds checking. + /// @param r The index of the row to access + /// @return A span representing the row at index r + /// @pre `r < n_rows()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed for performance. [[nodiscard]] row_type operator[](size_type r) { return row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); } + /// @brief Returns a const row at the given index without bounds checking. + /// @param r The index of the row to access + /// @return A const span representing the row at index r + /// @pre `r < n_rows()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] const_row_type operator[](size_type r) const { return const_row_type(this->_data.data() + r * this->_n_cols, this->_n_cols); } + /// @brief Returns a reference to an element without bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Reference to the element at the given coordinates + /// @pre `r < n_rows()` and `c < n_cols()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] reference operator[](size_type r, size_type c) { return this->_data[this->index(r, c)]; } + /// @brief Returns a const reference to an element without bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Const reference to the element at the given coordinates + /// @pre `r < n_rows()` and `c < n_cols()`; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] const_reference operator[](size_type r, size_type c) const { return this->_data[this->index(r, c)]; } + /// @brief Returns the row at the given index with bounds checking. + /// @param r The index of the row + /// @return A span representing the row at index r + /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] row_type at(size_type r) { this->_check_row(r); return (*this)[r]; } + /// @brief Returns a const row at the given index with bounds checking. + /// @param r The index of the row + /// @return A const span representing the row at index r + /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] const_row_type at(size_type r) const { this->_check_row(r); return (*this)[r]; } + /// @brief Returns a reference to an element with bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Reference to the element + /// @exception std::out_of_range If `r >= n_rows()` or `c >= n_cols()` [[nodiscard]] reference at(size_type r, size_type c) { this->_check_row(r); this->_check_col(c); return (*this)[r, c]; } + /// @brief Returns a const reference to an element with bounds checking. + /// @param r The row index + /// @param c The column index + /// @return Const reference to the element + /// @exception std::out_of_range If `r >= n_rows()` or `c >= n_cols()` [[nodiscard]] const_reference at(size_type r, size_type c) const { this->_check_row(r); this->_check_col(c); return (*this)[r, c]; } + /// @brief Returns the first row without bounds checking. + /// @return A span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] row_type front() noexcept { return (*this)[0uz]; } + /// @brief Returns a const reference to the first row without bounds checking. + /// @return A const span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] const_row_type front() const noexcept { return (*this)[0uz]; } + /// @brief Returns the last row without bounds checking. + /// @return A span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] row_type back() noexcept { return (*this)[this->_n_rows - 1uz]; } + /// @brief Returns a const reference to the last row without bounds checking. + /// @return A const span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] const_row_type back() const noexcept { return (*this)[this->_n_rows - 1uz]; } + /// @brief Explicitly named alias for `front()` yielding the first row. + /// @return A span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] row_type front_row() noexcept { return this->front(); } + /// @brief Explicitly named alias for `front()` yielding the first const row. + /// @return A const span representing the first row + /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] const_row_type front_row() const noexcept { return this->front(); } + /// @brief Explicitly named alias for `back()` yielding the last row. + /// @return A span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] row_type back_row() noexcept { return this->back(); } + /// @brief Explicitly named alias for `back()` yielding the last const row. + /// @return A const span representing the last row + /// @pre Matrix must not be empty; otherwise Undefined Behavior [[nodiscard]] const_row_type back_row() const noexcept { return this->back(); } + /// @brief Returns an unchecked $O(1)$ random-access view over the first column. + /// @return A strided view representing the first column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] auto front_col() noexcept { return this->_col_impl(0uz); } + /// @brief Returns an unchecked $O(1)$ random-access const view over the first column. + /// @return A strided view representing the const first column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] auto front_col() const noexcept { return this->_col_impl(0uz); } + /// @brief Returns an unchecked $O(1)$ random-access view over the last column. + /// @return A strided view representing the last column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] auto back_col() noexcept { return this->_col_impl(this->_n_cols - 1uz); } + /// @brief Returns an unchecked $O(1)$ random-access const view over the last column. + /// @return A strided view representing the const last column + /// @pre Matrix must not be empty; otherwise Undefined Behavior + /// @warning No bounds checking is performed. [[nodiscard]] auto back_col() const noexcept { return this->_col_impl(this->_n_cols - 1uz); } + /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked row. + /// @param r The row index + /// @return A span representing the row + /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] row_type row(size_type r) { return this->at(r); } + /// @brief Semantically symmetric alias for `at(r)` returning a bounds-checked const row. + /// @param r The row index + /// @return A const span representing the row + /// @exception std::out_of_range If `r >= n_rows()` [[nodiscard]] const_row_type row(size_type r) const { return this->at(r); } + /// @brief Returns a bounds-checked $O(1)$ random-access view over a specific column. + /// @param c The column index + /// @return A strided view representing the column + /// @exception std::out_of_range If `c >= n_cols()` [[nodiscard]] auto col(size_type c) { this->_check_col(c); return this->_col_impl(c); } + /// @brief Returns a bounds-checked $O(1)$ random-access const view over a specific column. + /// @param c The column index + /// @return A strided view representing the const column + /// @exception std::out_of_range If `c >= n_cols()` [[nodiscard]] auto col(size_type c) const { this->_check_col(c); return this->_col_impl(c); } + /// @brief Returns a view of all rows for iteration. + /// @return A random-access view of all row spans [[nodiscard]] auto rows() noexcept { return std::views::iota(size_type{0}, this->_n_rows) | std::views::transform([this](size_type i) -> row_type { return (*this)[i]; }); } + /// @brief Returns a const view of all rows for iteration. + /// @return A random-access const view of all row const spans [[nodiscard]] auto rows() const noexcept { return std::views::iota(size_type{0}, this->_n_rows) | std::views::transform([this](size_type i) -> const_row_type { return (*this)[i]; }); } + /// @brief Returns a view of all columns for iteration. + /// @return A random-access view of all column strided-views [[nodiscard]] auto cols() noexcept { return std::views::iota(size_type{0}, this->_n_cols) | std::views::transform([this](size_type c) { return this->_col_impl(c); }); } + /// @brief Returns a const view of all columns for iteration. + /// @return A random-access view of all const column strided-views [[nodiscard]] auto cols() const noexcept { return std::views::iota(size_type{0}, this->_n_cols) | std::views::transform([this](size_type c) { return this->_col_impl(c); }); @@ -423,50 +704,99 @@ class flat_matrix { // --- accessors (data) --- + /// @brief Returns the total number of elements structurally stored in the matrix. + /// @return The result of `n_rows() * n_cols()` [[nodiscard]] size_type data_size() const noexcept { return this->_data.size(); } + /// @brief Returns a span over all element data in flattened 1D form. + /// @return A span of all elements in the underlying `_data` array. [[nodiscard]] row_type data_view() noexcept { return row_type(this->_data); } + /// @brief Returns a const span over all element data in flattened 1D form. + /// @return A const span of all elements in the underlying `_data` array. [[nodiscard]] const_row_type data_view() const noexcept { return const_row_type(this->_data); } + /// @brief Returns a reference to the underlying flat data container. + /// @return A mutable reference to the underlying `_data` array. + /// @warning Modifying this vector directly can fatally corrupt the matrix structure. Use for advanced operations only. [[nodiscard]] std::vector& data_storage() noexcept { return this->_data; } + /// @brief Returns a const reference to the underlying flat data container. + /// @return A const reference to the underlying `_data` array. [[nodiscard]] const std::vector& data_storage() const noexcept { return this->_data; } + /// @brief Returns a raw pointer to the underlying flat data array. + /// @return A raw pointer to the first element in the `_data` array. + /// @warning No bounds checking is performed. [[nodiscard]] value_type* data_ptr() noexcept { return this->_data.data(); } + /// @brief Returns a const raw pointer to the underlying flat data array. + /// @return A const raw pointer to the first element in the `_data` array. [[nodiscard]] const value_type* data_ptr() const noexcept { return this->_data.data(); } // --- modifiers (rows) --- + /// @brief Appends a range as a new row at the bottom of the matrix. + /// @tparam R An input range of elements convertible to `value_type` + /// @param r The range to append + /// @post `n_rows()` increases by 1 + /// @exception std::invalid_argument If the row size does not match `n_cols()` (for non-empty matrices) + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. template requires std::convertible_to, value_type> void push_row(R&& r) { this->insert_row(this->_n_rows, std::forward(r)); } + /// @brief Appends an initializer list as a new row at the bottom of the matrix. + /// @param ilist The list to append + /// @post `n_rows()` increases by 1 + /// @exception std::invalid_argument If the list size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. void push_row(std::initializer_list ilist) { this->insert_row(this->_n_rows, std::span{ilist}); } + /// @brief Appends a newly created row filled with a specific value. + /// @param value The value to fill the new row with + /// @post `n_rows()` increases by 1 + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references if reallocation occurs. + /// @note **Time Complexity:** Amortized $O(C)$ where $C$ is the number of columns. void push_row(const value_type& value) { this->insert_row(this->_n_rows, value); } + /// @brief Inserts a new row at the specified position from a range. + /// @tparam R An input range of elements convertible to `value_type` + /// @param pos The row position where the elements will be inserted + /// @param r The range to insert + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::invalid_argument If the range size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @note Provides strong exception guarantee if size validation fails for unsized ranges. + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. template requires std::convertible_to, value_type> void insert_row(size_type pos, R&& r) { @@ -493,8 +823,6 @@ class flat_matrix { if (this->_n_rows == 0uz and this->_n_cols == 0uz) this->_n_cols = row_size; - this->_data.reserve(this->_data.size() + row_size); - if constexpr (std::ranges::contiguous_range) { auto* ptr = std::ranges::data(r); this->_data.insert(this->_data.begin() + insert_idx, ptr, ptr + row_size); @@ -532,10 +860,29 @@ class flat_matrix { ++this->_n_rows; } + /// @brief Inserts a new row at the specified position from an initializer list. + /// @param pos The row position where the elements will be inserted + /// @param ilist The list to insert + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::invalid_argument If the list size does not match `n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. void insert_row(size_type pos, std::initializer_list ilist) { this->insert_row(pos, std::span{ilist}); } + /// @brief Inserts a newly created row filled with a specific value at the specified position. + /// @param pos The row position where the elements will be inserted + /// @param value The value to fill the new row with + /// @post `n_rows()` increases by 1; rows at and after `pos` are shifted down + /// @exception std::out_of_range If `pos > n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning Invalidates all iterators, pointers, and references after the insertion point. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of total elements from `pos` onward + /// and $C$ is the size of the inserted row. void insert_row(size_type pos, const value_type& value) { if (pos > this->_n_rows) { throw std::out_of_range(std::format( @@ -549,6 +896,11 @@ class flat_matrix { ++this->_n_rows; } + /// @brief Removes the last row from the matrix. + /// @post If not empty, `n_rows()` decreases by 1 + /// @note Safe to call on an empty matrix (no-op). + /// @warning Invalidates all iterators, pointers, and references to elements in the last row. + /// @note **Time Complexity:** $O(C)$ to truncate the underlying `_data` vector. void pop_row() { if (this->empty()) return; @@ -560,6 +912,13 @@ class flat_matrix { this->_n_cols = 0uz; } + /// @brief Erases the row at the specified position. + /// @param pos The position of the row to erase + /// @post The row is removed; subsequent rows are shifted up; `n_rows()` decreases by 1 + /// @exception std::out_of_range If `pos >= n_rows()` + /// @warning Invalidates all iterators, pointers, and references at or after the erased position. + /// @note **Time Complexity:** $O(E + C)$ where $E$ is the number of elements after the erased row + /// and $C$ is the number of columns (the size of the erased row). void erase_row(size_type pos) { this->_check_row(pos); if (this->_n_rows == 1uz) { @@ -574,20 +933,55 @@ class flat_matrix { // --- modifiers (columns) --- + /// @brief Appends a range as a new column at the right edge of the matrix. + /// @tparam R An input range of elements convertible to `value_type` + /// @param r The range to append + /// @post `n_cols()` increases by 1 + /// @exception std::invalid_argument If the column size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. template requires std::convertible_to, value_type> void push_col(R&& r) { this->insert_col(this->_n_cols, std::forward(r)); } + /// @brief Appends an initializer list as a new column at the right edge of the matrix. + /// @param ilist The list to append + /// @post `n_cols()` increases by 1 + /// @exception std::invalid_argument If the list size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void push_col(std::initializer_list ilist) { this->insert_col(this->_n_cols, std::span{ilist}); } + /// @brief Appends a newly created column filled with a specific value at the right edge. + /// @param value The value to fill the new column with + /// @post `n_cols()` increases by 1 + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void push_col(const value_type& value) { this->insert_col(this->_n_cols, value); } + /// @brief Inserts a new column at the specified position from a range. + /// @tparam R An input range of elements convertible to `value_type` + /// @param pos The column position where elements will be inserted + /// @param r The range to insert + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::invalid_argument If the range size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. template requires std::convertible_to, value_type> void insert_col(size_type pos, R&& r) { @@ -612,7 +1006,7 @@ class flat_matrix { if (this->_n_rows == 0uz and this->_n_cols == 0uz) this->_n_rows = col_size; - // pre-allocate new vector to guarantee O(V^2) structural shift, avoiding O(V^3) with multiple inserts + // pre-allocate new vector to guarantee O(RxC) structural shift, avoiding cubic complexity with multiple inserts std::vector new_data; new_data.reserve(this->_n_rows * (this->_n_cols + 1uz)); @@ -646,10 +1040,29 @@ class flat_matrix { } } + /// @brief Inserts a new column at the specified position from an initializer list. + /// @param pos The column position where elements will be inserted + /// @param ilist The list to insert + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::invalid_argument If the list size does not match `n_rows()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void insert_col(size_type pos, std::initializer_list ilist) { this->insert_col(pos, std::span{ilist}); } + /// @brief Inserts a newly created column filled with a specific value at the specified position. + /// @param pos The column position where elements will be inserted + /// @param value The value to fill the new column with + /// @post `n_cols()` increases by 1 + /// @exception std::out_of_range If `pos > n_cols()` + /// @exception std::bad_alloc If memory allocation fails + /// @warning This operation forces a full reallocation and architectural shift of the mathematical grid. + /// All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void insert_col(size_type pos, const value_type& value) { if (pos > this->_n_cols) { throw std::out_of_range(std::format( @@ -682,6 +1095,11 @@ class flat_matrix { ++this->_n_cols; } + /// @brief Removes the last column from the matrix. + /// @post If not empty, `n_cols()` decreases by 1 + /// @note Safe to call on an empty matrix (no-op). + /// @warning This operation forces a reallocation and structural shift. All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void pop_col() { if (this->empty() || this->_n_cols == 0uz) return; @@ -689,6 +1107,12 @@ class flat_matrix { this->erase_col(this->_n_cols - 1uz); } + /// @brief Erases the column at the specified position. + /// @param pos The position of the column to erase + /// @post The column is removed; subsequent columns are mathematically shifted left; `n_cols()` decreases by 1 + /// @exception std::out_of_range If `pos >= n_cols()` + /// @warning This operation forces a reallocation and structural shift. All iterators, pointers, and references are invalidated. + /// @note **Time Complexity:** $O(R \times C)$ where $R$ and $C$ are dimensions of the matrix. void erase_col(size_type pos) { this->_check_col(pos); @@ -721,56 +1145,95 @@ class flat_matrix { // --- iterators --- + /// @brief Returns a mutable iterator to the first row. + /// @return Iterator to the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator begin() noexcept { return iterator(this->_data.data(), this->_n_cols, 0uz); } + /// @brief Returns a mutable iterator past the last row (end sentinel). + /// @return Iterator one position past the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] iterator end() noexcept { return iterator(this->_data.data(), this->_n_cols, this->_n_rows); } + /// @brief Returns a const iterator to the first row. + /// @return Const iterator to the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator begin() const noexcept { return const_iterator(this->_data.data(), this->_n_cols, 0uz); } + /// @brief Returns a const iterator past the last row (end sentinel). + /// @return Const iterator one position past the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator end() const noexcept { return const_iterator(this->_data.data(), this->_n_cols, this->_n_rows); } + /// @brief Returns a const iterator to the first row (explicit const form). + /// @return Const iterator to the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator cbegin() const noexcept { return this->begin(); } + /// @brief Returns a const iterator past the last row (explicit const form). + /// @return Const iterator one past the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_iterator cend() const noexcept { return this->end(); } + /// @brief Returns a reverse iterator to the last row. + /// @return Reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] reverse_iterator rbegin() noexcept { return reverse_iterator(this->end()); } + /// @brief Returns a reverse iterator before the first row (end sentinel). + /// @return Reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] reverse_iterator rend() noexcept { return reverse_iterator(this->begin()); } + /// @brief Returns a const reverse iterator to the last row. + /// @return Const reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_reverse_iterator rbegin() const noexcept { return const_reverse_iterator(this->end()); } + /// @brief Returns a const reverse iterator before the first row (end sentinel). + /// @return Const reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_reverse_iterator rend() const noexcept { return const_reverse_iterator(this->begin()); } + /// @brief Returns a const reverse iterator to the last row (explicit const form). + /// @return Const reverse iterator starting at the last row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_reverse_iterator crbegin() const noexcept { return this->rbegin(); } + /// @brief Returns a const reverse iterator before the first row (explicit const form). + /// @return Const reverse iterator one position before the first row + /// @note Iterator invalidated by structural modifications [[nodiscard]] const_reverse_iterator crend() const noexcept { return this->rend(); } // --- transformations --- + /// @brief Transposes the matrix mathematically (rows become columns, columns become rows). + /// @return A new `flat_matrix` instance containing the transposed data + /// @note **Time Complexity:** $O(R \times C)$ to generate and fill the new matrix. [[nodiscard]] flat_matrix transpose() const { flat_matrix result(this->_n_cols, this->_n_rows); for (size_type r = 0uz; r < this->_n_rows; ++r) @@ -780,6 +1243,10 @@ class flat_matrix { } private: + /// @brief Validates that the row index is within mathematical bounds. + /// @param r The row index to check + /// @exception std::out_of_range If `r >= n_rows()` + /// @note Used internally by checked accessors void _check_row(size_type r) const { if (r >= this->_n_rows) { throw std::out_of_range(std::format( @@ -790,6 +1257,10 @@ class flat_matrix { } } + /// @brief Validates that the column index is within mathematical bounds. + /// @param c The column index to check + /// @exception std::out_of_range If `c >= n_cols()` + /// @note Used internally by checked accessors void _check_col(size_type c) const { if (c >= this->_n_cols) { throw std::out_of_range(std::format( @@ -800,10 +1271,16 @@ class flat_matrix { } } + /// @brief Internal non-throwing helper generating a mutable strided view over a column. + /// @param c The column index (assumed valid) + /// @return A zero-overhead `std::views::stride` representing the column elements [[nodiscard]] auto _col_impl(size_type c) noexcept { return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); } + /// @brief Internal non-throwing helper generating a const strided view over a column. + /// @param c The column index (assumed valid) + /// @return A zero-overhead `std::views::stride` representing the const column elements [[nodiscard]] auto _col_impl(size_type c) const noexcept { return std::views::drop(this->_data, c) | std::views::stride(this->_n_cols); }