Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
002c14f
Enhance ring buffer implementation with additional features and tests
bugparty Jan 18, 2026
cdcb97c
Initial plan
Copilot Jan 18, 2026
4423210
Initial plan
Copilot Jan 18, 2026
8461f1d
Initial plan
Copilot Jan 18, 2026
4f3a415
Initial plan
Copilot Jan 18, 2026
cd69180
Add tests for move and swap operations with non-trivially copyable types
Copilot Jan 18, 2026
c72bd51
Enhance iterator equality tests based on review feedback
Copilot Jan 18, 2026
40a89ad
Final status update
Copilot Jan 18, 2026
cf5f003
Remove codeql build artifacts and update .gitignore
Copilot Jan 18, 2026
0b9d7d7
Finalize iterator equality test enhancements
Copilot Jan 18, 2026
55aec70
Update .gitignore to exclude CodeQL build directory
Copilot Jan 18, 2026
235c9ce
Merge pull request #4 from bugparty/copilot/sub-pr-2-again
bugparty Jan 18, 2026
879256e
Fix iterator equality operators to compare only source and index
Copilot Jan 18, 2026
da18e64
Fix iterator equality to compare only source and index
Copilot Jan 18, 2026
32b99a4
Improve code clarity based on code review
Copilot Jan 18, 2026
27807fd
Changes before error encountered
Copilot Jan 18, 2026
d49855f
Changes before error encountered
Copilot Jan 18, 2026
cc02225
Remove CodeQL build artifacts and update .gitignore
Copilot Jan 18, 2026
4a8f2ef
Merge pull request #6 from bugparty/copilot/sub-pr-2-yet-again
bugparty Jan 18, 2026
b068e63
Merge branch 'improvement' into copilot/sub-pr-2-another-one
bugparty Jan 18, 2026
c52050a
Remove CodeQL build artifacts and update .gitignore
Copilot Jan 18, 2026
e6cbd21
Merge pull request #5 from bugparty/copilot/sub-pr-2-another-one
bugparty Jan 18, 2026
d06bf03
Merge branch 'improvement' into copilot/sub-pr-2
bugparty Jan 18, 2026
bd7f23a
Remove redundant modulo operations and fix end() iterator
Copilot Jan 18, 2026
2e75f29
Merge pull request #3 from bugparty/copilot/sub-pr-2
bugparty Jan 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
.idea
cmake-build-debug
cmake-build-release
build
Testing/Temporary
_codeql_build_dir
_codeql_detected_source_root

# CodeQL build artifacts
_codeql_build_dir
_codeql_detected_source_root

32 changes: 18 additions & 14 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
cmake_minimum_required(VERSION 3.21)
project(RingBufferTest)
project(RingBuffer LANGUAGES CXX)

set(Simulate_Android_ToolChain OFF)
if (Simulate_Android_ToolChain)
Expand All @@ -9,19 +9,23 @@ else()
set(CMAKE_CXX_STANDARD 17)
endif()

# Add Google Test
# Include FetchContent to download and manage external dependencies
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip
)
FetchContent_MakeAvailable(googletest)
add_library(RingBuffer INTERFACE)
add_library(RingBuffer::RingBuffer ALIAS RingBuffer)
target_include_directories(RingBuffer INTERFACE ${CMAKE_CURRENT_SOURCE_DIR})

enable_testing()
add_executable(RingBufferTest test_main.cpp)
target_link_libraries(RingBufferTest gtest gtest_main)
option(RINGBUFFER_BUILD_TESTS "Build RingBuffer tests" ON)
if (RINGBUFFER_BUILD_TESTS)
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/refs/tags/release-1.11.0.zip
)
FetchContent_MakeAvailable(googletest)

enable_testing()
add_executable(RingBufferTest test_main.cpp)
target_link_libraries(RingBufferTest gtest gtest_main RingBuffer::RingBuffer)

include(GoogleTest)
gtest_discover_tests(RingBufferTest)
include(GoogleTest)
gtest_discover_tests(RingBufferTest)
endif()
37 changes: 35 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,8 @@ This project demonstrates many modern C++ best practices:
### Build Instructions

```bash
git clone https://github.com/yourusername/RingBuffer.git
cd RingBuffer
git clone https://github.com/bugparty/RingBufferCpp.git
cd RingBufferCpp
mkdir build && cd build
cmake ..
cmake --build .
Expand All @@ -60,6 +60,39 @@ This will:

---

## FetchContent Integration

```cmake
include(FetchContent)
FetchContent_Declare(
RingBuffer
GIT_REPOSITORY https://github.com/bugparty/RingBufferCpp.git
GIT_TAG main
)
FetchContent_MakeAvailable(RingBuffer)

target_link_libraries(your_target PRIVATE RingBuffer::RingBuffer)
```

To skip building tests in your parent project, set:

```cmake
set(RINGBUFFER_BUILD_TESTS OFF)
```

---

## Testing Policy

When changing the library, run the tests before pushing changes:

```bash
cmake --build build
ctest --output-on-failure --test-dir build
```

---

Usage

```cpp
Expand Down
122 changes: 114 additions & 8 deletions RingBuffer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <type_traits>
#include <algorithm>
#include <cstring>
#include <utility>
#include <vector>
#pragma once
namespace buffers {
Expand Down Expand Up @@ -62,12 +63,16 @@ namespace buffers {
[[nodiscard]] const_pointer operator->() const noexcept {
return &((*source_)[index_]);
}
[[nodiscard]] self_type& operator++() noexcept {
index_ = ++index_ % N;
self_type& operator++() noexcept {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [[nodiscard]] attribute was removed from the prefix increment operator. This attribute is useful for preventing accidental use as a statement (e.g., ++it; when the return value is important). While prefix increment is commonly used for its side effect, having [[nodiscard]] can help catch bugs where the user meant to use the result. Consider keeping this attribute for consistency with modern C++ best practices.

Suggested change
self_type& operator++() noexcept {
[[nodiscard]] self_type& operator++() noexcept {

Copilot uses AI. Check for mistakes.
++count_;
if (count_ >= source_->size()) {
index_ = N; // Set to sentinel value (out of valid range [0, N-1]) when reaching end
} else {
index_ = (index_ + 1) % N;
}
return *this;
}
[[nodiscard]] self_type operator++(int) noexcept {
self_type operator++(int) noexcept {
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [[nodiscard]] attribute was removed from the postfix increment operator. This is actually less appropriate than the prefix case, as postfix increment returns a copy of the iterator before incrementing. The return value is often meaningful, and discarding it might indicate a mistake (the user should use prefix increment if they don't need the old value). Consider restoring this attribute.

Suggested change
self_type operator++(int) noexcept {
[[nodiscard]] self_type operator++(int) noexcept {

Copilot uses AI. Check for mistakes.
auto result = *this;
++(*this);
return result;
Expand All @@ -78,6 +83,9 @@ namespace buffers {
[[nodiscard]] size_type count() const noexcept {
return count_;
}
[[nodiscard]] buffer_t source() const noexcept {
return source_;
}
~ring_buffer_iterator() = default;
private:
buffer_t source_{};
Expand All @@ -88,13 +96,13 @@ namespace buffers {
template<typename T, size_t N, bool C, bool Overwrite>
bool operator==(ring_buffer_iterator<T,N,C,Overwrite> const& l,
ring_buffer_iterator<T,N,C,Overwrite> const& r) noexcept {
return l.count() == r.count();
return l.source() == r.source() && l.index() == r.index();
}

template<typename T, size_t N, bool C, bool Overwrite>
bool operator!=(ring_buffer_iterator<T,N,C,Overwrite> const& l,
ring_buffer_iterator<T,N,C,Overwrite> const& r) noexcept {
return l.count() != r.count();
return l.source() != r.source() || l.index() != r.index();
}

}
Expand Down Expand Up @@ -134,11 +142,14 @@ using std::bool_constant;
using iterator = detail::ring_buffer_iterator<T, N, false, Overwrite>;
using const_iterator = detail::ring_buffer_iterator<T, N, true, Overwrite>;

// Create an empty ring buffer.
ring_buffer() noexcept = default;
// Copy contents and state from another buffer.
ring_buffer(ring_buffer const& rhs) noexcept(is_nothrow_copy_constructible_v<value_type>)
{
copy_impl(rhs, bool_constant<is_trivially_copyable_v<T>>{});
}
// Assign from another buffer.
ring_buffer& operator=(ring_buffer const& rhs) noexcept(is_nothrow_copy_constructible_v<value_type>) {
if(this == &rhs)
return *this;
Expand All @@ -148,10 +159,31 @@ using std::bool_constant;

return *this;
}
// Move contents and state from another buffer.
ring_buffer(ring_buffer&& rhs) noexcept(std::is_nothrow_move_constructible<value_type>::value)
{
move_impl(rhs, bool_constant<is_trivially_copyable_v<T>>{});
}
// Move-assign from another buffer.
ring_buffer& operator=(ring_buffer&& rhs) noexcept(std::is_nothrow_move_constructible<value_type>::value) {
if(this == &rhs)
return *this;

destroy_all(bool_constant<is_trivially_copyable_v<T>>{});
move_impl(rhs, bool_constant<is_trivially_copyable_v<T>>{});

return *this;
}
// Swap contents with another buffer.
void swap(self_type& rhs) noexcept(noexcept(swap_impl(rhs, bool_constant<is_trivially_copyable_v<T>>{}))) {
swap_impl(rhs, bool_constant<is_trivially_copyable_v<T>>{});
}
// Append an element, overwriting when configured.
template<typename U>
void push_back(U&& value) {
push_back(std::forward<U>(value), bool_constant<Overwrite>{});
}
// Remove the oldest element if present.
void pop_front() noexcept{
if(empty())
return;
Expand All @@ -161,36 +193,52 @@ using std::bool_constant;
--size_;
tail_ = ++tail_ %N;
}
// Access the newest element.
[[nodiscard]] reference back() noexcept {
return reinterpret_cast<reference>(elements_[(head_ + N - 1) % N]);
}
// Access the newest element (const).
[[nodiscard]] const_reference back() const noexcept {
return const_cast<self_type*>(this)->back();
}
// Access the oldest element.
[[nodiscard]] reference front() noexcept { return reinterpret_cast<reference >(elements_[tail_]); }
// Access the oldest element (const).
[[nodiscard]] const_reference front() const noexcept {
return const_cast<self_type*>(this)->front();
}
// Direct access by internal storage index.
[[nodiscard]] reference operator[](size_type index) noexcept {
return reinterpret_cast<reference >(elements_[index]);
}
// Direct access by internal storage index (const).
[[nodiscard]] const_reference operator[](size_type index) const noexcept {
return const_cast<self_type *>(this)->operator[](index);
}
// Iterator to oldest element.
[[nodiscard]] iterator begin() noexcept { return iterator{this, tail_, 0};}
[[nodiscard]] iterator end() noexcept { return iterator{this, head_, size_};}
// Iterator to one past newest element.
[[nodiscard]] iterator end() noexcept { return iterator{this, N, size_};}
// Const iterator to oldest element.
[[nodiscard]] const_iterator cbegin() const noexcept { return const_iterator{this, tail_, 0};}
[[nodiscard]] const_iterator cend() const noexcept { return const_iterator{this, head_, size_};}
// Const iterator to one past newest element.
[[nodiscard]] const_iterator cend() const noexcept { return const_iterator{this, N, size_};}
// Check if buffer has no elements.
[[nodiscard]] bool empty() const noexcept { return size_ == 0; }
// Check if buffer is at capacity.
[[nodiscard]] bool full() const noexcept { return size_ == N; }
// Current element count.
[[nodiscard]] size_type size() const noexcept { return size_; }
// Maximum element count.
[[nodiscard]] size_type capacity() const noexcept { return N; }
// Remove all elements and reset indices.
void clear() noexcept {
destroy_all(bool_constant<is_trivially_destructible_v<value_type>>{});
size_ = 0;
head_ = 0;
tail_ = 0;
}
// Destroy elements on teardown.
~ring_buffer() {
clear();
};
Expand All @@ -204,7 +252,7 @@ using std::bool_constant;
}
}
void copy_impl(self_type const& rhs, std::true_type) {
std::memcpy(elements_, rhs.elements_, rhs.size_ * sizeof(T));
std::memcpy(elements_, rhs.elements_, N * sizeof(T));
size_ = rhs.size_;
tail_ = rhs.tail_;
head_ = rhs.head_;
Expand Down Expand Up @@ -248,6 +296,59 @@ using std::bool_constant;

#endif
}
void move_impl(self_type& rhs, std::true_type) {
std::memcpy(elements_, rhs.elements_, N * sizeof(T));
size_ = rhs.size_;
tail_ = rhs.tail_;
head_ = rhs.head_;
rhs.clear();
}
void move_impl(self_type& rhs, std::false_type) {
tail_ = rhs.tail_;
head_ = rhs.head_;
size_ = rhs.size_;
#ifdef __cpp_exceptions
try {
for (auto i = 0; i < size_; ++i)
new( elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N]));
}catch(...) {
while(!empty()) {
destroy(tail_, bool_constant<std::is_trivially_destructible_v<value_type>>{});
tail_ = ++tail_ % N;
--size_;
Comment on lines +314 to +318

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Track constructed elements when move throws

If T's move constructor throws during move_impl (non-trivial path), the catch block destroys elements in a loop driven by size_, which was set to rhs.size_ before construction. When the throw happens partway through, only some slots were constructed, so this loop can call destroy on uninitialized storage, which is undefined behavior and can crash for types with throwing moves. This can surface in move construction, move assignment, and swap_impl for such types. Consider tracking the number of successfully constructed elements (or updating size_ incrementally) and only destroying those.

Useful? React with 👍 / 👎.

}
throw;
}
#else
storage_type *p = nullptr;
for (auto i = 0; i < size_; ++i) {
p =reinterpret_cast<storage_type *>(new(elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N])));
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after equals sign. Should be 'p = reinterpret_cast' instead of 'p =reinterpret_cast' for consistency with code formatting standards.

Copilot uses AI. Check for mistakes.
if (!p) {
break;
}
}
if (!p) {
while(!empty()) {
destroy(tail_, bool_constant<is_trivially_destructible_v<value_type>>{});
tail_ = ++tail_ % N;
--size_;
}
}
Comment on lines +323 to +336
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The placement new expression will never return nullptr. Placement new either successfully constructs the object or throws an exception (if exceptions are enabled). The check 'if (!p)' will never be true because placement new always returns the pointer passed to it. This error handling logic is ineffective and should be removed.

Suggested change
storage_type *p = nullptr;
for (auto i = 0; i < size_; ++i) {
p =reinterpret_cast<storage_type *>(new(elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N])));
if (!p) {
break;
}
}
if (!p) {
while(!empty()) {
destroy(tail_, bool_constant<is_trivially_destructible_v<value_type>>{});
tail_ = ++tail_ % N;
--size_;
}
}
for (auto i = 0; i < size_; ++i) {
new(elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N]));
}

Copilot uses AI. Check for mistakes.
#endif
rhs.clear();
}
Comment on lines +299 to +339
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Fix exception-safety in move_impl for non‑trivial types.
If a move construction throws, the current cleanup can destroy uninitialized storage because size_ is preset to rhs.size_. Track constructed count and destroy only those, then set size_ after success.

🐛 Proposed fix: track constructed elements and set size_ after success
-        void move_impl(self_type& rhs, std::false_type) {
-            tail_ = rhs.tail_;
-            head_ = rhs.head_;
-            size_ = rhs.size_;
+        void move_impl(self_type& rhs, std::false_type) {
+            tail_ = rhs.tail_;
+            size_ = 0;
 `#ifdef` __cpp_exceptions
-            try {
-                for (auto i = 0; i < size_; ++i)
-                    new( elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N]));
-            }catch(...) {
-                while(!empty()) {
-                    destroy(tail_, bool_constant<std::is_trivially_destructible_v<value_type>>{});
-                    tail_ = ++tail_ % N;
-                    --size_;
-                }
-                throw;
-            }
+            size_type constructed = 0;
+            try {
+                for (; constructed < rhs.size_; ++constructed) {
+                    new(elements_ + ((tail_ + constructed) % N))
+                        T(std::move(rhs[(tail_ + constructed) % N]));
+                }
+            } catch (...) {
+                while (constructed > 0) {
+                    --constructed;
+                    destroy((tail_ + constructed) % N,
+                            bool_constant<is_trivially_destructible_v<value_type>>{});
+                }
+                head_ = tail_;
+                size_ = 0;
+                throw;
+            }
+            size_ = rhs.size_;
 `#else`
-            storage_type *p = nullptr;
-            for (auto i = 0; i < size_; ++i) {
-                p =reinterpret_cast<storage_type *>(new(elements_ + ((tail_ + i) % N)) T(std::move(rhs[(tail_ + i) % N])));
-                if (!p) {
-                    break;
-                }
-            }
-            if (!p) {
-                while(!empty()) {
-                    destroy(tail_, bool_constant<is_trivially_destructible_v<value_type>>{});
-                    tail_ = ++tail_ % N;
-                    --size_;
-                }
-            }
+            size_type constructed = 0;
+            for (; constructed < rhs.size_; ++constructed) {
+                new(elements_ + ((tail_ + constructed) % N))
+                    T(std::move(rhs[(tail_ + constructed) % N]));
+            }
+            size_ = constructed;
 `#endif`
+            head_ = (tail_ + size_) % N;
             rhs.clear();
         }
🤖 Prompt for AI Agents
In `@RingBuffer.hpp` around lines 295 - 335, In move_impl(self_type& rhs,
std::false_type) you must not set size_ to rhs.size_ before element
move-construction because a thrown exception will cause destroy(...) to run on
uninitialized slots; instead track how many elements you actually constructed
(e.g., constructed_count), leave size_ at 0 during construction, for-loop
constructing elements_ + ((tail_ + i) % N) from rhs and increment
constructed_count, on catch/detected failure destroy only the constructed_count
items (using destroy(tail_ + offset, ...)), adjust tail_/head_ as needed while
decrementing constructed_count, rethrow the exception (or handle failure path),
and only after all moves succeed set size_ = rhs.size_ and finally rhs.clear();
apply the same constructed-count pattern in both the __cpp_exceptions and the
no-exceptions branch and reference elements_, tail_, head_, size_, destroy,
storage_type and rhs.clear() when making the changes.

void swap_impl(self_type& rhs, std::true_type) noexcept {
std::swap(elements_, rhs.elements_);
std::swap(head_, rhs.head_);
std::swap(tail_, rhs.tail_);
std::swap(size_, rhs.size_);
}
Comment on lines +340 to +345
Copy link

Copilot AI Jan 18, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The swap_impl for trivially copyable types attempts to swap C-style arrays (elements_), which cannot be done with std::swap. C-style arrays are not assignable. This implementation will not compile. The correct approach is to swap element-by-element or use std::swap_ranges for the arrays.

Copilot uses AI. Check for mistakes.
void swap_impl(self_type& rhs, std::false_type) {
self_type temp;
temp.move_impl(*this, std::false_type{});
this->move_impl(rhs, std::false_type{});
rhs.move_impl(temp, std::false_type{});
}
Comment on lines +346 to +351
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

swap_impl for non-trivial types lacks exception safety.

This implementation uses three move_impl calls through a temporary. If move_impl throws during the second or third move:

  • Line 345: If this throws, *this is already cleared by line 344, and rhs is partially moved. Both buffers may be in inconsistent states.
  • Line 346: If this throws, *this has rhs's old data, but rhs is cleared and temp still holds *this's original data.

The strong exception guarantee cannot be provided with this approach when moves can throw. Consider documenting that swap provides only basic exception guarantee, or implement a more careful rollback mechanism.

🤖 Prompt for AI Agents
In `@RingBuffer.hpp` around lines 342 - 347, The swap_impl(self_type& rhs,
std::false_type) implementation is not exception-safe when move_impl can throw;
if the second or third move_impl throws the two buffers can be left in
inconsistent states. Fix by either documenting that swap_impl only offers the
basic exception guarantee or implement a rollback: perform moves into
temporaries (use a local temp for *this and another temp for rhs), then commit
by moving temporaries into targets only after all moves succeed, or catch
exceptions and restore original state by moving back from any succeeded
temporaries; update references to swap_impl, move_impl and
self_type/std::false_type accordingly so the logic uses fail-safe temporaries
and a clear commit/rollback path.

template<typename U>
void push_back(U&& value, std::true_type) {
push_back_impl(std::forward<U>(value));
Expand Down Expand Up @@ -284,5 +385,10 @@ using std::bool_constant;
size_type size_{};
};

template<typename T, size_t N, bool Overwrite>
void swap(ring_buffer<T, N, Overwrite>& lhs, ring_buffer<T, N, Overwrite>& rhs) noexcept(noexcept(lhs.swap(rhs))) {
lhs.swap(rhs);
}

}
#endif //RINGBUFFERTEST_RINGBUFFER_HPP
1 change: 1 addition & 0 deletions _codeql_detected_source_root
Loading