Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

XMas-Day-1: HistoryBuffer: added push_front method, iterator constraints, and resize #490

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

RalphSteinhagen
Copy link
Member

On the first day of Christmas 🎄 👼 1️⃣ 🎄 , my code base gave to me...

A ring buffer with push_front and pop—how jolly can that be?

This PR refines the HistoryBuffer by introducing or improving:

  1. `push_front(...)`: Lets you store elements so the oldest ends up at index [0].
  2. `pop_front()` & `pop_back()`: Remove elements from the logical front/back of the ring.
  3. `front()` & `back()`: Simple, direct access to the first/last elements in logical order.
  4. `resize(size_t)` (for dynamic-extent): Resize capacity while retaining existing items.
  5. Documentation: We’ve updated our doxygen comments to show how these new methods work, plus concise code examples illustrating “newest-at-[0]” vs. “oldest-at-[0].”

Along the way, we trimmed some unneeded modulo operations for single-step increments, simplified loops with std::copy, and tidied up our tests.

Feel free to test it out and give feedback.
Happy coding—and happy holidays!

Code Example:

gr::history_buffer<int, 5> hb_newest;   // Use push_back -> index[0] is newest
hb_newest.push_back(1); // [1]
hb_newest.push_back(2); // [2, 1]
hb_newest.push_back(3); // [3, 2, 1]
// => index[0] == 3, newest item

gr::history_buffer<int, 5> hb_oldest;   // Use push_front -> index[0] is oldest
hb_oldest.push_front(10); // [10]
hb_oldest.push_front(20); // [10, 20]
hb_oldest.push_front(30); // [10, 20, 30]
// => index[0] == 10, oldest item

hb_newest.pop_front();  // remove newest => now hb_newest: [2, 1]
hb_oldest.pop_front();  // remove oldest => now hb_oldest: [20, 30]

auto val = hb_newest.front();  // val == 2
auto last = hb_newest.back();  // last == 1

- provide push_front(const T&) and push_front(Iter, Iter) for reversed usage
- provide pop_front() and pop_back() to have a deque-like behaviour
- provide resize(std::size_t) to adjust capacity
- enforce std::input_iterator and std::ranges::range in push_back_bulk/push_[front, back]
- added minimal unit tests for resize and front()/back()

Signed-off-by: Ralph J. Steinhagen <[email protected]>
@RalphSteinhagen RalphSteinhagen changed the title HistoryBuffer: added push_front method, iterator constraints, and resize XMas-Day-1: HistoryBuffer: added push_front method, iterator constraints, and resize Dec 25, 2024
const auto nSamplesToCopy = std::distance(cbegin, cend);
Iter optimizedBegin = static_cast<std::size_t>(nSamplesToCopy) > _capacity ? std::prev(cend, static_cast<std::ptrdiff_t>(_capacity)) : cbegin;
for (auto it = optimizedBegin; it != cend; ++it) {
push_back(*it);
}
}

template<std::input_iterator Iter>
Copy link
Contributor

Choose a reason for hiding this comment

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

Why a separate name for the new push_back?

IMO, it should be named append_range as in std::vector, though that one takes a range and not a pair of iterators. We could have append_range(begin, end) and append_range(range) -- if the functions below also become append_range?

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't want to break existing code, thus keeping the "old" push_back_bulk(...). From a templated API point of view there is no need to distinguish to distinguish between single sample or bulk operation. The implementation is different (for efficiency reasons) but the API should stay the same.

push_back and `push_front' keep the symmetry between reverse (nominal) and forward (new optional) behaviour.

Copy link
Contributor

Choose a reason for hiding this comment

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

I didn't want to break existing code, thus keeping the "old" push_back_bulk(...).

I was hoping we're still in the 'we can break API things' as we didn't have a release. (and we do break behaviour in other places from time to time)

Copy link
Member Author

Choose a reason for hiding this comment

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

Generally yes, but didn't want to change the whole code base and do the follow-up in gr-digitizer and OpenDigitizer ... yet.

constexpr void push_back_bulk(const Range& range) noexcept {
push_back_bulk(range.cbegin(), range.cend());
}

/**
* @brief Adds an element to the front expiring the oldest element beyond the buffer's capacities.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure what this means.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's still a circular/limited buffer size. If you add a sample (either front or back) the oldest is being dropped once the maximum size is reached. This is the difference to the std::deque behaviour.

Copy link
Contributor

Choose a reason for hiding this comment

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

No, I get that, just wanted to say the comment itself is not that clear :)

* the "newest" in the ring, with operator[](0) always the oldest.
*/
template<std::input_iterator Iter>
constexpr void push_front(Iter first, Iter last) noexcept {
Copy link
Contributor

Choose a reason for hiding this comment

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

Following the previous comment, this would be prepend_range.

Copy link
Member Author

Choose a reason for hiding this comment

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

I went for the std::deque API syntax with some sprinkles of std::vector<T>. Hope this make sense. Didn't want to break too much of the existing API/use elsewhere.

:-)

Copy link
Contributor

Choose a reason for hiding this comment

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

std::deque also has single-value push_back/push_front and append_range/prepend_range for multiple values.

Copy link
Member Author

Choose a reason for hiding this comment

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

Didn't know about 'append_range/prepend_range' yet (it's C++23 newish). Shall we add this? I like the overloaded push_back/push_front alone because semantically there isn't much difference other than that the ..._range also describe the type of function argument.

core/include/gnuradio-4.0/HistoryBuffer.hpp Outdated Show resolved Hide resolved
core/include/gnuradio-4.0/HistoryBuffer.hpp Outdated Show resolved Hide resolved
core/include/gnuradio-4.0/HistoryBuffer.hpp Outdated Show resolved Hide resolved
Copy link

Quality Gate Failed Quality Gate failed

Failed conditions
72.2% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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