diff --git a/.gitignore b/.gitignore index 608b1d4..b12a39b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .vscode/ -build/ \ No newline at end of file +build/ +_codeql_build_dir/ +_codeql_detected_source_root \ No newline at end of file diff --git a/include/nd_array/nd_array.hpp b/include/nd_array/nd_array.hpp index d792297..08c9b70 100644 --- a/include/nd_array/nd_array.hpp +++ b/include/nd_array/nd_array.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -156,6 +157,202 @@ namespace cppa seen[axis] = true; } } + /// \brief Stride-aware random access iterator for nd_span + /// \tparam ElementType Element type (use const-qualified type for a const iterator) + /// \tparam MaxRank Maximum number of dimensions + template + class nd_iterator + { + public: + using size_type = size_t; + using pointer = ElementType*; + using difference_type = std::ptrdiff_t; + using value_type = std::remove_cv_t; + using reference = ElementType&; + using iterator_category = std::random_access_iterator_tag; + + nd_iterator( ) = default; + + /// \brief Constructs an iterator at a given flat position within the span + /// \param t_data Pointer to the first element of the span + /// \param t_extents Extent (size) of each dimension + /// \param t_strides Stride of each dimension + /// \param t_rank Number of active dimensions + /// \param t_flat_start Starting flat index (0 = begin, total_size = end) + nd_iterator( pointer t_data, const std::array& t_extents, const std::array& t_strides, size_type t_rank, + size_type t_flat_start = 0 ) + : m_data( t_data ) + , m_extents( t_extents ) + , m_strides( t_strides ) + , m_rank( t_rank ) + , m_flat_size( compute_size( t_extents, t_rank ) ) + , m_flat_index( t_flat_start ) + { + if( m_flat_index > m_flat_size ) + { + m_flat_index = m_flat_size; + } + update_from_flat_index( ); + } + + /// \brief Allow implicit conversion from non-const to const iterator + template && !std::is_const_v, int> = 0> + nd_iterator( const nd_iterator& t_other ) + : m_data( t_other.m_data ) + , m_ptr( t_other.m_ptr ) + , m_extents( t_other.m_extents ) + , m_strides( t_other.m_strides ) + , m_indices( t_other.m_indices ) + , m_rank( t_other.m_rank ) + , m_flat_size( t_other.m_flat_size ) + , m_flat_index( t_other.m_flat_index ) + { + } + + // --- Element access --- + + [[nodiscard]] reference operator*( ) const { return *m_ptr; } + [[nodiscard]] pointer operator->( ) const { return m_ptr; } + + /// \brief Returns a reference to the element at offset n from this iterator + [[nodiscard]] reference operator[]( difference_type t_n ) const + { + nd_iterator tmp = *this; + tmp += t_n; + return *tmp; + } + + // --- Increment / decrement --- + + nd_iterator& operator++( ) + { + advance( 1 ); + return *this; + } + + nd_iterator operator++( int ) + { + nd_iterator tmp = *this; + advance( 1 ); + return tmp; + } + + nd_iterator& operator--( ) + { + advance( -1 ); + return *this; + } + + nd_iterator operator--( int ) + { + nd_iterator tmp = *this; + advance( -1 ); + return tmp; + } + + // --- Arithmetic --- + + nd_iterator& operator+=( difference_type t_n ) + { + advance( t_n ); + return *this; + } + + nd_iterator& operator-=( difference_type t_n ) + { + advance( -t_n ); + return *this; + } + + [[nodiscard]] nd_iterator operator+( difference_type t_n ) const + { + nd_iterator tmp = *this; + tmp += t_n; + return tmp; + } + + [[nodiscard]] nd_iterator operator-( difference_type t_n ) const + { + nd_iterator tmp = *this; + tmp -= t_n; + return tmp; + } + + [[nodiscard]] friend nd_iterator operator+( difference_type t_n, const nd_iterator& t_it ) { return t_it + t_n; } + + /// \brief Returns the signed distance between two iterators from the same span + [[nodiscard]] difference_type operator-( const nd_iterator& t_other ) const + { + return static_cast( m_flat_index ) - static_cast( t_other.m_flat_index ); + } + + // --- Comparison --- + + [[nodiscard]] bool operator==( const nd_iterator& t_other ) const { return m_flat_index == t_other.m_flat_index; } + [[nodiscard]] bool operator!=( const nd_iterator& t_other ) const { return !( *this == t_other ); } + [[nodiscard]] bool operator<( const nd_iterator& t_other ) const { return m_flat_index < t_other.m_flat_index; } + [[nodiscard]] bool operator>( const nd_iterator& t_other ) const { return t_other < *this; } + [[nodiscard]] bool operator<=( const nd_iterator& t_other ) const { return !( *this > t_other ); } + [[nodiscard]] bool operator>=( const nd_iterator& t_other ) const { return !( *this < t_other ); } + + private: + pointer m_data = nullptr; + pointer m_ptr = nullptr; + std::array m_extents{ }; + std::array m_strides{ }; + std::array m_indices{ }; + size_type m_rank = 0; + size_type m_flat_size = 0; + size_type m_flat_index = 0; + + // Grant access to the opposite const-ness specialisation for the converting constructor + friend class nd_iterator, std::remove_const_t, const ElementType>, MaxRank>; + + /// \brief Updates the multi-dimensional indices and element pointer from the current flat index + void update_from_flat_index( ) + { + if( m_flat_index >= m_flat_size ) + { + m_ptr = nullptr; + return; + } + // Decompose flat index into per-dimension indices (row-major) + size_type f = m_flat_index; + for( int i = static_cast( m_rank ) - 1; i >= 0; --i ) + { + m_indices[i] = f % m_extents[i]; + f /= m_extents[i]; + } + // Compute element pointer using strides + m_ptr = m_data; + for( size_type i = 0; i < m_rank; ++i ) + { + m_ptr += m_indices[i] * m_strides[i]; + } + } + + /// \brief Moves the iterator by t_n positions (positive = forward, negative = backward) + void advance( difference_type t_n ) + { + if( t_n == 0 ) + return; + const auto signed_index = static_cast( m_flat_index ) + t_n; + if( signed_index < 0 ) + { + m_flat_index = 0; + } + else if( static_cast( signed_index ) >= m_flat_size ) + { + m_flat_index = m_flat_size; + } + else + { + m_flat_index = static_cast( signed_index ); + } + update_from_flat_index( ); + } + }; + } // namespace detail /// \class nd_span @@ -514,23 +711,26 @@ namespace cppa /// \return Const pointer to the first element [[nodiscard]] const_pointer data( ) const noexcept { return m_data; } - /// \brief Returns a pointer to the first element for flat iteration - [[nodiscard]] pointer begin( ) noexcept { return m_data; } + using iterator = detail::nd_iterator; ///< Mutable stride-aware iterator + using const_iterator = detail::nd_iterator; ///< Const stride-aware iterator - /// \brief Returns a pointer past the last element for flat iteration - [[nodiscard]] pointer end( ) noexcept { return m_data + size( ); } + /// \brief Returns a stride-aware iterator to the first element + [[nodiscard]] iterator begin( ) noexcept { return iterator( m_data, m_extents, m_strides, m_rank ); } - /// \brief Returns a const pointer to the first element for flat iteration - [[nodiscard]] const_pointer begin( ) const noexcept { return m_data; } + /// \brief Returns a past-the-end iterator + [[nodiscard]] iterator end( ) noexcept { return iterator( m_data, m_extents, m_strides, m_rank, size( ) ); } - /// \brief Returns a const pointer past the last element for flat iteration - [[nodiscard]] const_pointer end( ) const noexcept { return m_data + size( ); } + /// \brief Returns a stride-aware const iterator to the first element + [[nodiscard]] const_iterator begin( ) const noexcept { return const_iterator( m_data, m_extents, m_strides, m_rank ); } - /// \brief Returns a const pointer to the first element for flat iteration - [[nodiscard]] const_pointer cbegin( ) const noexcept { return m_data; } + /// \brief Returns a past-the-end const iterator + [[nodiscard]] const_iterator end( ) const noexcept { return const_iterator( m_data, m_extents, m_strides, m_rank, size( ) ); } - /// \brief Returns a const pointer past the last element for flat iteration - [[nodiscard]] const_pointer cend( ) const noexcept { return m_data + size( ); } + /// \brief Returns a stride-aware const iterator to the first element + [[nodiscard]] const_iterator cbegin( ) const noexcept { return const_iterator( m_data, m_extents, m_strides, m_rank ); } + + /// \brief Returns a past-the-end const iterator + [[nodiscard]] const_iterator cend( ) const noexcept { return const_iterator( m_data, m_extents, m_strides, m_rank, size( ) ); } private: pointer m_data; ///< Pointer to the first element diff --git a/tests/test_nd_span.cpp b/tests/test_nd_span.cpp index 7155393..ea31c8d 100644 --- a/tests/test_nd_span.cpp +++ b/tests/test_nd_span.cpp @@ -2,11 +2,18 @@ #include #include +#include #include using namespace cppa; +// Verify iterator category at compile time +static_assert( std::is_same_v::iterator>::iterator_category, std::random_access_iterator_tag>, + "nd_span::iterator must be a random access iterator" ); +static_assert( std::is_same_v::const_iterator>::iterator_category, std::random_access_iterator_tag>, + "nd_span::const_iterator must be a random access iterator" ); + TEST_CASE( "nd_span - Construction", "[nd_span][construction]" ) { SECTION( "Variadic constructor - 1D" ) @@ -463,3 +470,153 @@ TEST_CASE( "nd_span - Integration with nd_array", "[nd_span][integration]" ) REQUIRE( arr( 1, 0 ) == 99 ); } } + +TEST_CASE( "nd_span - Stride-aware iteration", "[nd_span][iterators][stride]" ) +{ + SECTION( "Iteration over column subspan respects strides" ) + { + // 3x5 matrix with sequential values 0..14 + std::array data = { }; + for( size_t i = 0; i < 15; ++i ) + { + data[i] = static_cast( i ); + } + + nd_span span( data.data( ), 3, 5 ); + // Take columns 1-3 (subspan with non-unit stride in dim 0) + auto sub = span.subspan( 1, { 1, 4 } ); // extents [3,3], strides [5,1] + + std::vector values; + for( auto v: sub ) + { + values.push_back( v ); + } + + // Expected row-major traversal: (0,0)=1, (0,1)=2, (0,2)=3, + // (1,0)=6, (1,1)=7, (1,2)=8, + // (2,0)=11,(2,1)=12,(2,2)=13 + REQUIRE( values.size( ) == 9 ); + REQUIRE( values[0] == 1 ); + REQUIRE( values[1] == 2 ); + REQUIRE( values[2] == 3 ); + REQUIRE( values[3] == 6 ); + REQUIRE( values[4] == 7 ); + REQUIRE( values[5] == 8 ); + REQUIRE( values[6] == 11 ); + REQUIRE( values[7] == 12 ); + REQUIRE( values[8] == 13 ); + } + + SECTION( "Iteration over transposed span respects strides" ) + { + // 2x3 matrix: 1 2 3 / 4 5 6 + std::array data = { 1, 2, 3, 4, 5, 6 }; + nd_span span( data.data( ), 2, 3 ); // strides [3,1] + + auto t = span.T( ); // extents [3,2], strides [1,3] + + std::vector values; + for( auto v: t ) + { + values.push_back( v ); + } + + // Expected row-major traversal of transposed view: + // t(0,0)=1, t(0,1)=4, t(1,0)=2, t(1,1)=5, t(2,0)=3, t(2,1)=6 + REQUIRE( values.size( ) == 6 ); + REQUIRE( values[0] == 1 ); + REQUIRE( values[1] == 4 ); + REQUIRE( values[2] == 2 ); + REQUIRE( values[3] == 5 ); + REQUIRE( values[4] == 3 ); + REQUIRE( values[5] == 6 ); + } + + SECTION( "Write through iterator over subspan modifies correct elements" ) + { + std::array data = { }; + nd_span span( data.data( ), 4, 5 ); + + // Take rows 1-2 (subspan along dim 0) + auto sub = span.subspan( 0, { 1, 3 } ); // extents [2,5], strides [5,1] + int val = 1; + for( auto& v: sub ) + { + v = val++; + } + + // Rows 0 and 3 should be untouched + for( size_t j = 0; j < 5; ++j ) + { + REQUIRE( span( 0, j ) == 0 ); + REQUIRE( span( 3, j ) == 0 ); + } + // Rows 1-2 should have values 1-10 + REQUIRE( span( 1, 0 ) == 1 ); + REQUIRE( span( 1, 4 ) == 5 ); + REQUIRE( span( 2, 0 ) == 6 ); + REQUIRE( span( 2, 4 ) == 10 ); + } + + SECTION( "Random access operations on contiguous span" ) + { + std::array data = { 10, 20, 30, 40, 50, 60 }; + nd_span span( data.data( ), 2, 3 ); + + auto it = span.begin( ); + + // operator[] relative to begin + REQUIRE( it[0] == 10 ); + REQUIRE( it[5] == 60 ); + + // operator+ / operator- + REQUIRE( *( it + 2 ) == 30 ); + REQUIRE( *( 2 + it ) == 30 ); + + // advance and retreat + it += 4; + REQUIRE( *it == 50 ); + it -= 3; + REQUIRE( *it == 20 ); + + // iterator difference + auto it2 = span.begin( ) + 4; + REQUIRE( it2 - span.begin( ) == 4 ); + REQUIRE( span.end( ) - span.begin( ) == 6 ); + + // ordering + REQUIRE( span.begin( ) < span.end( ) ); + REQUIRE( span.end( ) > span.begin( ) ); + REQUIRE( span.begin( ) <= span.begin( ) ); + REQUIRE( span.end( ) >= span.end( ) ); + + // pre/post decrement + auto it3 = span.end( ); + --it3; + REQUIRE( *it3 == 60 ); + it3--; + REQUIRE( *it3 == 50 ); + } + + SECTION( "Random access arithmetic over non-contiguous subspan" ) + { + // 3x5 matrix, values 0..14 + std::array data = { }; + for( size_t i = 0; i < 15; ++i ) + { + data[i] = static_cast( i ); + } + + nd_span span( data.data( ), 3, 5 ); + auto sub = span.subspan( 1, { 1, 4 } ); // extents [3,3], strides [5,1] + + // begin()[4] should be the 5th element in flat traversal order: (1,1)=7 + REQUIRE( sub.begin( )[4] == 7 ); + + // end - begin == size + REQUIRE( sub.end( ) - sub.begin( ) == static_cast( sub.size( ) ) ); + + // advance begin by size reaches end + REQUIRE( sub.begin( ) + static_cast( sub.size( ) ) == sub.end( ) ); + } +}