Skip to content

Conversation

@bugparty
Copy link
Owner

@bugparty bugparty commented Jan 18, 2026

  • Updated CMakeLists.txt to define project and include Google Test conditionally.
  • Expanded .gitignore to include build artifacts and temporary testing files.
  • Improved RingBuffer class with move semantics, swap functionality, and additional iterator methods.
  • Added comprehensive unit tests for copy behavior, iterator equality, and buffer state after operations.
  • Updated README with FetchContent integration instructions and testing policy.

Important

Enhance RingBuffer with move semantics, swap functionality, iterator improvements, and add comprehensive tests.

  • RingBuffer Enhancements:
    • Added move semantics and swap functionality to ring_buffer in RingBuffer.hpp.
    • Enhanced iterator methods in ring_buffer_iterator in RingBuffer.hpp.
  • CMake and Build:
    • Updated CMakeLists.txt to conditionally include Google Test based on RINGBUFFER_BUILD_TESTS option.
    • Added RingBuffer as an interface library in CMakeLists.txt.
  • Documentation:
    • Updated README.md with FetchContent integration instructions and testing policy.
  • Testing:
    • Added unit tests in test_main.cpp for move semantics, swap functionality, iterator equality, and buffer state after operations.

This description was created by Ellipsis for 002c14f. You can customize this summary. It will automatically update as commits are pushed.

Summary by CodeRabbit

  • New Features

    • Added move/swap support, element accessors (front/back/index), pop_front, clear, public iterators, and state queries (size/capacity/empty/full)
  • Documentation

    • Expanded README with build/setup steps, FetchContent integration examples, usage snippets, and a testing policy (includes how to skip building tests)
  • Tests

    • Large new test suite covering moves, swaps, iteration, wrap/overwrite, and edge cases; test builds can be toggled via a configuration option
  • Chores

    • Updated ignore patterns and added repository housekeeping files

✏️ Tip: You can customize this high-level summary in your review settings.

- Updated CMakeLists.txt to define project and include Google Test conditionally.
- Expanded .gitignore to include build artifacts and temporary testing files.
- Improved RingBuffer class with move semantics, swap functionality, and additional iterator methods.
- Added comprehensive unit tests for copy behavior, iterator equality, and buffer state after operations.
- Updated README with FetchContent integration instructions and testing policy.
Copilot AI review requested due to automatic review settings January 18, 2026 09:51
@coderabbitai
Copy link

coderabbitai bot commented Jan 18, 2026

Note

Other AI code review bot(s) detected

CodeRabbit has detected other AI code review bot(s) in this pull request and will avoid duplicating their findings in the review comments. This may lead to a less comprehensive review.

📝 Walkthrough

Walkthrough

Adds copy/move/swap semantics, richer element accessors and iterator state to RingBuffer; introduces an INTERFACE RingBuffer target and a conditional RINGBUFFER_BUILD_TESTS CMake option; adds extensive unit tests; updates README with FetchContent usage; and expands .gitignore and CodeQL marker file.

Changes

Cohort / File(s) Summary
Build system
CMakeLists.txt
Add option(RINGBUFFER_BUILD_TESTS ...); gate FetchContent/GoogleTest and test targets behind the option; create add_library(RingBuffer INTERFACE) and alias RingBuffer::RingBuffer; set project(... LANGUAGES CXX)
Core library
RingBuffer.hpp
Add <utility>; implement copy/move constructors and assignments; add member and non-member swap; add forwarding push_back(U&&) and internal dispatch; add pop_front, front/back, const/non-const operator[]; expose begin/end/cbegin/cend; add clear, empty, full, size, capacity; extend iterator with source() and update iterator increment/equality semantics; add SFINAE helpers for move/copy/swap implementations
Tests
test_main.cpp
Add ~20+ unit tests covering copy/move/swap semantics, iterator behavior (including across wrapped buffers), overwrite/no-overwrite behavior, pop/clear, and tests with trivial and non-trivial types (std::string, std::vector)
Docs & ignore files
README.md, .gitignore, _codeql_detected_source_root
README: update build instructions, FetchContent example, usage and testing policy; .gitignore: add build, Testing/Temporary, _codeql_build_dir, _codeql_detected_source_root; add _codeql_detected_source_root file containing .

Sequence Diagram(s)

(Skipped.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

codex

Poem

🐰 I hopped through headers, moved and swapped with glee,

I fronted and backed each element carefully,
CMake now asks if tests should run or rest,
Docs and tests together put changes to the test,
Hooray — the buffer’s nimble now, at its best!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: enhancements to the ring buffer implementation with new features (move semantics, swap, accessors) and comprehensive test additions.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@ellipsis-dev ellipsis-dev bot left a comment

Choose a reason for hiding this comment

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

Important

Looks good to me! 👍

Reviewed everything up to 002c14f in 2 minutes and 21 seconds. Click for details.
  • Reviewed 587 lines of code in 4 files
  • Skipped 1 files when reviewing.
  • Skipped posting 7 draft comments. View those below.
  • Modify your settings and rules to customize what types of comments Ellipsis leaves. And don't forget to react with 👍 or 👎 to teach Ellipsis.
1. CMakeLists.txt:16
  • Draft comment:
    Good use of the RINGBUFFER_BUILD_TESTS option and FetchContent integration for googletest.
  • Reason this comment was not posted:
    Confidence changes required: 0% <= threshold 50% None
2. README.md:61
  • Draft comment:
    The FetchContent integration instructions and testing policy updates are clear and helpful.
  • Reason this comment was not posted:
    Confidence changes required: 0% <= threshold 50% None
3. RingBuffer.hpp:95
  • Draft comment:
    Improved iterator equality now compares source, count, and index—this enhances correctness when comparing iterators from different buffers.
  • Reason this comment was not posted:
    Confidence changes required: 0% <= threshold 50% None
4. RingBuffer.hpp:337
  • Draft comment:
    Swapping the C-style array 'elements_' using std::swap may not work as intended. Consider using std::swap_ranges (or wrapping the storage in a std::array) to ensure all elements are correctly exchanged.
  • Reason this comment was not posted:
    Decided after close inspection that this draft comment was likely wrong and/or not actionable: usefulness confidence = 10% vs. threshold = 50% The comment is about swapping C-style arrays. In C++, std::swap on C-style arrays actually DOES work correctly since C++11 - it swaps the entire array element-by-element. The standard library has a specialization for arrays. So the comment appears to be incorrect. The code std::swap(elements_, rhs.elements_) should work fine for swapping two C-style arrays of the same type and size. The suggestion to use std::swap_ranges is unnecessary. This appears to be a case where the automated tool is not aware that std::swap handles arrays correctly in modern C++. I should double-check my understanding of std::swap with C-style arrays. While I believe it works correctly in C++11 and later, I want to make sure I'm not missing something. Also, the code is trying to maintain C++14 compatibility (based on the #if (__cplusplus < 201703L) checks), so I should verify this works in C++14 as well. After reconsidering, I'm confident that std::swap has been able to handle C-style arrays correctly since C++11 via template specialization. The standard library provides template<class T, size_t N> void swap(T (&a)[N], T (&b)[N]) which swaps arrays element-by-element. Since this codebase appears to target at least C++14 (based on the version checks), this functionality is available. The comment is incorrect. The comment should be deleted because it's based on incorrect information. std::swap works correctly with C-style arrays in C++11 and later, which this codebase supports. The suggested change to std::swap_ranges is unnecessary and would not improve the code.
5. test_main.cpp:240
  • Draft comment:
    Tests are comprehensive; consider adding cases for move‐only types and exception safety for non‐trivial types to further validate move semantics.
  • Reason this comment was not posted:
    Confidence changes required: 33% <= threshold 50% None
6. RingBuffer.hpp:201
  • Draft comment:
    Typo: There's an extra space inside the angle brackets in reinterpret_cast<reference >(elements_[tail_]). Consider removing the space to be consistent with other casts (i.e. reinterpret_cast<reference>(elements_[tail_])).
  • Reason this comment was not posted:
    Comment was on unchanged code.
7. RingBuffer.hpp:208
  • Draft comment:
    Typo: There's an extra space inside the angle brackets in reinterpret_cast<reference >(elements_[index]). It should be reinterpret_cast<reference>(elements_[index]).
  • Reason this comment was not posted:
    Comment was on unchanged code.

Workflow ID: wflow_iQtjd5IsoY5a2LE9

You can customize Ellipsis by changing your verbosity settings, reacting with 👍 or 👎, replying to comments, or adding code review rules.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 002c14f8d3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +310 to +314
}catch(...) {
while(!empty()) {
destroy(tail_, bool_constant<std::is_trivially_destructible_v<value_type>>{});
tail_ = ++tail_ % N;
--size_;

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 👍 / 👎.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR enhances the ring buffer implementation with move semantics, swap functionality, iterator improvements, and comprehensive testing. It also updates the build system to create a proper CMake interface library and adds documentation for FetchContent integration.

Changes:

  • Added move constructor, move assignment operator, and swap member/non-member functions to RingBuffer
  • Enhanced iterator equality operators to compare source buffer in addition to position
  • Updated CMakeLists.txt to create an interface library target with optional test building
  • Added 14 new comprehensive unit tests covering edge cases, move semantics, and swap operations

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
RingBuffer.hpp Added move semantics, swap functionality, improved iterator comparison, and inline documentation comments
test_main.cpp Added comprehensive tests for copy behavior, iterator equality, move/swap operations, and edge cases
CMakeLists.txt Restructured to create RingBuffer interface library with conditional test building
README.md Updated repository URLs, added FetchContent integration guide and testing policy
.gitignore Added build directory and testing temporary files

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

#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.
RingBuffer.hpp Outdated
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.count() != r.count() || l.index() != r.index();
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.

Similar to the equality operator, the inequality operator should be based on De Morgan's law applied to the correct equality comparison. If equality should only compare source and index (not count), then inequality should be: l.source() != r.source() || l.index() != r.index()

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

return &((*source_)[index_]);
}
[[nodiscard]] self_type& operator++() noexcept {
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.
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.
Comment on lines +270 to +337
TEST(RingBufferTest, MoveConstructionTransfersState) {
ring_buffer<int, 4> b1;
b1.push_back(1);
b1.push_back(2);
b1.push_back(3);
b1.pop_front();
b1.push_back(4);

ring_buffer<int, 4> b2(std::move(b1));

EXPECT_TRUE(b1.empty());
EXPECT_EQ(b2.size(), 3);
EXPECT_EQ(b2.front(), 2);
EXPECT_EQ(b2.back(), 4);
}

TEST(RingBufferTest, MoveAssignmentTransfersState) {
ring_buffer<int, 3> b1;
b1.push_back(5);
b1.push_back(6);

ring_buffer<int, 3> b2;
b2.push_back(10);
b2 = std::move(b1);

EXPECT_TRUE(b1.empty());
EXPECT_EQ(b2.size(), 2);
EXPECT_EQ(b2.front(), 5);
EXPECT_EQ(b2.back(), 6);
}

TEST(RingBufferTest, SwapExchangesBuffers) {
ring_buffer<int, 3> b1;
b1.push_back(1);
b1.push_back(2);

ring_buffer<int, 3> b2;
b2.push_back(9);
b2.push_back(10);
b2.push_back(11);

b1.swap(b2);

EXPECT_EQ(b1.size(), 3);
EXPECT_EQ(b1.front(), 9);
EXPECT_EQ(b1.back(), 11);
EXPECT_EQ(b2.size(), 2);
EXPECT_EQ(b2.front(), 1);
EXPECT_EQ(b2.back(), 2);
}

TEST(RingBufferTest, NonMemberSwapWorks) {
ring_buffer<int, 2> b1;
b1.push_back(3);
b1.push_back(4);

ring_buffer<int, 2> b2;
b2.push_back(7);

using std::swap;
swap(b1, b2);

EXPECT_EQ(b1.size(), 1);
EXPECT_EQ(b1.front(), 7);
EXPECT_EQ(b2.size(), 2);
EXPECT_EQ(b2.front(), 3);
EXPECT_EQ(b2.back(), 4);
}
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.

All new move and swap tests only test trivially copyable types (int). The move_impl and swap_impl methods have separate code paths for non-trivially copyable types, but these are not tested. Consider adding tests for move construction, move assignment, and swap operations with non-trivially copyable types like std::vector or std::string to ensure both code paths work correctly.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +336 to +341
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_);
}
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.
Comment on lines +319 to +332
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_;
}
}
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.
RingBuffer.hpp Outdated
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.count() == r.count() && l.index() == r.index();
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 iterator equality comparison now requires all three fields (source, count, and index) to match. However, for correct iterator semantics, two iterators should be equal if they refer to the same position in the same container, regardless of their count value. The count field tracks how many times the iterator has been incremented and is not relevant for equality. This could cause issues where logically equivalent iterators (same buffer, same position) are incorrectly considered unequal if they were created through different paths. Consider comparing only source and index.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Comment on lines 118 to 123
TEST(RingBufferTest, IteratorsFromDifferentBuffersAreNotEqual) {
ring_buffer<int, 3> b1;
ring_buffer<int, 3> b2;

EXPECT_NE(b1.cbegin(), b2.cbegin());
}
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.

This test checks that iterators from different buffers are not equal, which is good. However, it only tests begin iterators from empty buffers. The test should also verify that iterators at the same logical position but from different buffers are unequal, and that iterators from the same buffer at the same position are equal. Consider adding test cases that verify: (1) begin() == begin() for the same buffer, (2) begin() != end() for the same buffer when not empty.

Copilot uses AI. Check for mistakes.
Copy link
Owner Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback

Copy link
Contributor

Copilot AI commented Jan 18, 2026

@bugparty I've opened a new pull request, #3, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Jan 18, 2026

@bugparty I've opened a new pull request, #4, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Jan 18, 2026

@bugparty I've opened a new pull request, #5, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI commented Jan 18, 2026

@bugparty I've opened a new pull request, #6, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@RingBuffer.hpp`:
- Around line 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.

Comment on lines +295 to +335
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_;
}
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])));
if (!p) {
break;
}
}
if (!p) {
while(!empty()) {
destroy(tail_, bool_constant<is_trivially_destructible_v<value_type>>{});
tail_ = ++tail_ % N;
--size_;
}
}
#endif
rhs.clear();
}
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.

Copilot AI and others added 9 commits January 18, 2026 10:03
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Add test coverage for move/swap operations with non-trivially copyable types
- Updated operator== to compare source and index (not count)
- Updated operator!= using De Morgan's law
- Modified operator++ to set index to sentinel value N when reaching end
- Changed end() and cend() to use index N instead of head_
- This provides proper iterator semantics where iterators are equal if they point to the same position in the same buffer

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Modified iterator implementation to remove count from equality comparison:
- Changed increment operator to not wrap index (now stores unwrapped index)
- Updated dereference operators to apply modulo when accessing elements
- Modified end() to use tail_ + size_ instead of head_ for unique index
- Removed count from operator== and operator!= comparisons

This ensures two iterators are equal if they point to the same position
in the same container, regardless of iteration count, following standard
iterator semantics.

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
- Changed pre-increment expression to clearer form: (index_ + 1) % N
- Enhanced comment to clarify sentinel value purpose

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Copilot AI and others added 8 commits January 18, 2026 10:09
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Removed _codeql_build_dir directory and _codeql_detected_source_root
symlink that were accidentally committed. These are build artifacts from
the CodeQL security scanner and should not be in version control.

Updated .gitignore to prevent these files from being committed in the
future.

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
Enhance iterator equality test coverage
- Removed _codeql_build_dir directory (build artifacts)
- Removed _codeql_detected_source_root file
- Added CodeQL artifacts to .gitignore to prevent future commits

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
[WIP] Update to address feedback on ring buffer enhancement
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
RingBuffer.hpp (1)

256-294: Fix exception-safety in copy_impl for non-trivial types.

This method has the same exception-safety bug as move_impl: size_ is set to rhs.size_ at line 259 before any elements are constructed. If a copy constructor throws mid-loop (line 265), the catch block's while(!empty()) loop will destroy uninitialized storage—undefined behavior.

Additionally, the non-exception path (lines 275-291) checks if (!p) after placement new, but placement new never returns nullptr; it either succeeds or throws.

🐛 Proposed fix: track constructed elements
         void copy_impl(self_type const& rhs, std::false_type) {
             tail_ = rhs.tail_;
-            head_ = rhs.head_;
-            size_ = rhs.size_;
+            size_ = 0;
 `#ifdef` __cpp_exceptions
+            size_type constructed = 0;
             try {
-                for (auto i = 0; i < size_; ++i)
-                    // construct value in memory of aligned storage
-                    // using inplace operator new
-                    new( elements_ + ((tail_ + i) % N)) T(rhs[(tail_ + i) % N]);
-            }catch(...) {
-                while(!empty()) {
-                    destroy(tail_, bool_constant<std::is_trivially_destructible_v<value_type>>{});
-                    tail_ = ++tail_ % N;
-                    --size_;
+                for (; constructed < rhs.size_; ++constructed) {
+                    new(elements_ + ((tail_ + constructed) % N)) T(rhs[(tail_ + constructed) % N]);
                 }
+            } catch (...) {
+                while (constructed > 0) {
+                    --constructed;
+                    destroy((tail_ + constructed) % N,
+                            bool_constant<is_trivially_destructible_v<value_type>>{});
+                }
                 throw;
             }
+            size_ = rhs.size_;
+            head_ = rhs.head_;
 `#else`
-            storage_type *p = nullptr;
-            for (auto i = 0; i < size_; ++i) {
-
-                // construct value in memory of aligned storage
-                // using inplace operator new
-                p =reinterpret_cast<storage_type *>(new(elements_ + ((tail_ + i) % N)) T(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 (size_type i = 0; i < rhs.size_; ++i) {
+                new(elements_ + ((tail_ + i) % N)) T(rhs[(tail_ + i) % N]);
             }
-
+            size_ = rhs.size_;
+            head_ = rhs.head_;
 `#endif`
         }
🤖 Fix all issues with AI agents
In `@RingBuffer.hpp`:
- Around line 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.
♻️ Duplicate comments (3)
RingBuffer.hpp (3)

66-74: Consider restoring [[nodiscard]] on increment operators.

The [[nodiscard]] attribute was removed from both prefix and postfix increment. For postfix increment especially, discarding the return value often indicates a mistake (prefer prefix if the old value isn't needed).


302-335: Exception-safety issue in move_impl for non-trivial types remains unaddressed.

Setting size_ = rhs.size_ before construction (line 305) means if a move constructor throws, the cleanup loop destroys uninitialized storage. The placement-new nullptr check (lines 322-324) is also ineffective.


336-341: swap_impl for trivially copyable types will not compile.

std::swap(elements_, rhs.elements_) attempts to swap C-style arrays, which are not assignable. This line will fail to compile.

🧹 Nitpick comments (1)
RingBuffer.hpp (1)

206-213: Clarify operator[] semantics in documentation.

The operator[] accesses elements by internal storage index rather than logical position from the oldest element. The comment says "Direct access by internal storage index" which is accurate, but users might expect buffer[0] to return the oldest element (like front()).

Consider either:

  1. Renaming to a less public-API-like name (e.g., at_raw_index)
  2. Adding a note that this is primarily for internal use

Comment on lines +342 to +347
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{});
}
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.

Copilot AI and others added 2 commits January 18, 2026 11:29
- Removed % N from operator* and operator-> since index_ is always in [0, N-1] for valid iterators
- Changed end() and cend() to use N as sentinel value, consistent with operator++ implementation
- With the current operator++ that keeps index_ bounded and sets it to N at end, the modulo in dereference operators is redundant

Co-authored-by: bugparty <1510776+bugparty@users.noreply.github.com>
[WIP] Update ring buffer enhancement based on review feedback
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
RingBuffer.hpp (2)

186-194: Undefined behavior in C++14: unsequenced modification.

The expression tail_ = ++tail_ % N on line 194 has two modifications to tail_ that are unsequenced in C++14 and earlier, causing undefined behavior. Since the code includes C++14 backports (lines 109-124), this is a compatibility concern.

This pattern appears in multiple locations: lines 194, 250, 273, 292, 317, 333, 369, 372.

🐛 Proposed fix for line 194
-            tail_ = ++tail_ %N;
+            tail_ = (tail_ + 1) % N;

Apply the same fix pattern to all other occurrences.


260-297: Exception-safety issue: same pattern as move_impl.

This function has the same exception-safety flaw as move_impl: size_ is set to rhs.size_ on line 263 before any elements are constructed. If construction throws partway through, the cleanup loop will call destroy on uninitialized storage.

Additionally, the placement new nullptr check (lines 285-295) is ineffective since placement new never returns nullptr.

🐛 Proposed fix: track constructed count
         void copy_impl(self_type const& rhs, std::false_type) {
             tail_ = rhs.tail_;
-            head_ = rhs.head_;
-            size_ = rhs.size_;
+            size_ = 0;
 `#ifdef` __cpp_exceptions
+            size_type constructed = 0;
             try {
-                for (auto i = 0; i < size_; ++i)
-                    // construct value in memory of aligned storage
-                    // using inplace operator new
-                    new( elements_ + ((tail_ + i) % N)) T(rhs[(tail_ + i) % N]);
-            }catch(...) {
-                while(!empty()) {
-                    destroy(tail_, bool_constant<std::is_trivially_destructible_v<value_type>>{});
-                    tail_ = ++tail_ % N;
-                    --size_;
+                for (; constructed < rhs.size_; ++constructed) {
+                    new(elements_ + ((tail_ + constructed) % N)) T(rhs[(tail_ + constructed) % N]);
                 }
+            } catch (...) {
+                while (constructed > 0) {
+                    --constructed;
+                    destroy((tail_ + constructed) % N, bool_constant<is_trivially_destructible_v<value_type>>{});
+                }
                 throw;
             }
+            size_ = rhs.size_;
+            head_ = rhs.head_;
 `#else`
-            storage_type *p = nullptr;
-            for (auto i = 0; i < size_; ++i) {
-
-                // construct value in memory of aligned storage
-                // using inplace operator new
-                p =reinterpret_cast<storage_type *>(new(elements_ + ((tail_ + i) % N)) T(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 (size_type i = 0; i < rhs.size_; ++i) {
+                new(elements_ + ((tail_ + i) % N)) T(rhs[(tail_ + i) % N]);
             }
-
+            size_ = rhs.size_;
+            head_ = rhs.head_;
 `#endif`
         }
♻️ Duplicate comments (4)
RingBuffer.hpp (4)

66-78: Consider restoring [[nodiscard]] on increment operators.

The [[nodiscard]] attribute was removed from both prefix and postfix increment operators. For postfix operator++(int), discarding the return value often indicates the programmer should use prefix increment instead. Consider restoring this attribute to catch potential misuse.


299-338: Exception-safety and code issues remain unaddressed.

The previous review flagged critical issues in move_impl that are still present:

  1. Exception safety (lines 306-321): size_ is set before construction; if a move throws, destroy is called on uninitialized storage.
  2. Ineffective null check (lines 323-336): Placement new never returns nullptr, making this error handling dead code.
  3. Formatting (line 325): Missing space after = in p =reinterpret_cast.

Please refer to the previous review comments for the detailed fix.


340-345: std::swap on C-style arrays will not compile.

Line 341 attempts std::swap(elements_, rhs.elements_), but C-style arrays are not assignable and cannot be swapped directly with std::swap. This code will fail to compile when swap_impl is instantiated for trivially copyable types.

🐛 Proposed fix: swap element-by-element or use std::swap_ranges
         void swap_impl(self_type& rhs, std::true_type) noexcept {
-            std::swap(elements_, rhs.elements_);
+            for (size_type i = 0; i < N; ++i) {
+                std::swap(elements_[i], rhs.elements_[i]);
+            }
             std::swap(head_, rhs.head_);
             std::swap(tail_, rhs.tail_);
             std::swap(size_, rhs.size_);
         }
#!/bin/bash
# Verify that std::swap on C-style arrays fails to compile
cat > /tmp/test_swap.cpp << 'EOF'
`#include` <utility>
`#include` <algorithm>

struct alignas(int) storage_t { char data[sizeof(int)]; };

int main() {
    storage_t a[5]{};
    storage_t b[5]{};
    std::swap(a, b);  // Should fail: arrays are not assignable
    return 0;
}
EOF
g++ -std=c++17 -c /tmp/test_swap.cpp 2>&1 || echo "Compilation failed as expected"

346-351: swap_impl for non-trivial types provides only basic exception guarantee.

As flagged previously, if move_impl throws during the second or third move operation, both buffers may be left in inconsistent states. Consider documenting that swap provides only the basic exception guarantee for types with throwing move constructors.

🧹 Nitpick comments (1)
RingBuffer.hpp (1)

196-217: Consider documenting preconditions for element accessors.

back(), front(), and operator[] do not check if the buffer is empty or if the index is valid. This is consistent with standard library containers, but the undefined behavior on empty buffers should be documented for users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants