Skip to content

Commit 487827f

Browse files
committed
Range-Based API
1 parent cf2e866 commit 487827f

4 files changed

Lines changed: 326 additions & 5 deletions

File tree

examples/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,8 @@ add_executable(advanced_usage advanced_usage.cpp)
1313
# Link the example to our queue library and the threading library.
1414
target_link_libraries(advanced_usage PRIVATE spsc_queue Threads::Threads)
1515

16+
# --- Example for Ranges API ---
17+
add_executable(ranges_example ranges_example.cpp)
18+
19+
target_link_libraries(ranges_example PRIVATE spsc_queue Threads::Threads)
20+

examples/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,73 @@ void low_level_batch_consumer(LockFreeSpscQueue<Message>& queue)
181181
// The read is automatically committed when `read_scope` is destroyed.
182182
}
183183
```
184+
185+
## 4. Range-Based API (`std::ranges`)
186+
187+
For maximum convenience and compatibility with modern C++ algorithms, both the `WriteScope` and `ReadScope` objects are fully compliant with the C++20 `std::ranges` library. This allows for direct, elegant iteration, completely abstracting away the two-block nature of the circular buffer.
188+
189+
### Writing with a Range-Based `for` Loop
190+
191+
This is a clear and idiomatic way to fill the reserved slots in a write transaction.
192+
193+
```cpp
194+
void range_based_producer(LockFreeSpscQueue<int>& queue)
195+
{
196+
// Ask to reserve space for 10 items.
197+
if (auto write_scope = queue.prepare_write(10)) {
198+
// The WriteScope object is directly iterable.
199+
// We can iterate over the empty slots and write to them.
200+
int i = 0;
201+
for (int& slot : write_scope) {
202+
slot = i++;
203+
}
204+
}
205+
// The write is automatically committed when `write_scope` is destroyed.
206+
}
207+
```
208+
209+
### Reading with a Range-Based `for` Loop
210+
211+
This is the simplest way to consume data from the queue. The custom iterator handles the jump between the two memory blocks transparently.
212+
213+
```cpp
214+
void range_based_consumer(LockFreeSpscQueue<int>& queue)
215+
{
216+
// Ask to read up to 16 items at a time.
217+
if (auto read_scope = queue.prepare_read(16))
218+
{
219+
// The ReadScope object is a C++20 range.
220+
// The for loop will seamlessly iterate over block1 and then block2.
221+
for (const int& item : read_scope)
222+
{
223+
std::cout << "Consumer: Got " << item << "\n";
224+
}
225+
}
226+
// The read is automatically committed when `read_scope` is destroyed.
227+
}
228+
```
229+
230+
### Using with `std::ranges` Algorithms
231+
232+
Because the `Scope` objects are proper ranges, you can use them with the powerful algorithms from the `<algorithm>` and `<numeric>` headers.
233+
234+
```cpp
235+
#include <numeric> // For std::accumulate
236+
#include <algorithm> // For std::ranges::copy
237+
238+
void algorithm_example(LockFreeSpscQueue<int>& queue)
239+
{
240+
// Use std::ranges::copy to fill a write scope from another container.
241+
if (auto write_scope = queue.prepare_write(10)) {
242+
std::vector<int> source_data(write_scope.get_items_written(), 42); // Fill with 42s
243+
std::ranges::copy(source_data, write_scope.begin());
244+
}
245+
246+
// Use std::accumulate to sum all the items in a read scope.
247+
if (auto read_scope = queue.prepare_read(10)) {
248+
long long sum = std::accumulate(read_scope.begin(), read_scope.end(), 0LL);
249+
std::cout << "Sum of items in queue: " << sum << "\n";
250+
}
251+
}
252+
```
253+

examples/ranges_example.cpp

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
#include "LockFreeSpscQueue.h"
2+
3+
#include <iostream>
4+
#include <vector>
5+
#include <thread>
6+
#include <atomic>
7+
#include <chrono>
8+
#include <numeric> // For std::iota
9+
#include <algorithm> // For std::ranges::copy
10+
11+
// This example demonstrates the `std::ranges` integration of the WriteScope
12+
// and ReadScope objects. It shows how to use modern, idiomatic C++ to interact
13+
// with the queue, abstracting away the underlying two-block nature of the
14+
// circular buffer.
15+
16+
// --- Producer Thread ---
17+
void producer_thread(LockFreeSpscQueue<int>& queue)
18+
{
19+
std::cout << "Producer: Starting...\n";
20+
21+
// --- Method 1: Writing with a range-based for loop ---
22+
std::cout << "Producer: Preparing to write items 0-9 using a for loop...\n";
23+
while (true) {
24+
auto write_scope = queue.prepare_write(10);
25+
// Check if the scope is valid by checking the number of items.
26+
if (write_scope.get_items_written() > 0)
27+
{
28+
int i = 0;
29+
for (int& slot : write_scope) {
30+
slot = i++;
31+
}
32+
std::cout << "Producer: Wrote " << write_scope.get_items_written() << " items.\n";
33+
break;
34+
}
35+
std::this_thread::yield();
36+
}
37+
38+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
39+
40+
// --- Method 2: Batch Writing with a std::ranges algorithm ---
41+
std::cout << "Producer: Preparing to write items 100-115 using std::ranges::copy...\n";
42+
std::vector<int> source_data(16);
43+
std::iota(source_data.begin(), source_data.end(), 100);
44+
45+
size_t total_written = 0;
46+
while (total_written < source_data.size())
47+
{
48+
auto write_scope = queue.prepare_write(source_data.size() - total_written);
49+
// Check if the scope is valid.
50+
if (write_scope.get_items_written() > 0)
51+
{
52+
const size_t can_write_count = write_scope.get_items_written();
53+
std::span<const int> source_sub_batch(source_data.data() + total_written,
54+
can_write_count);
55+
std::ranges::copy(source_sub_batch, write_scope.begin());
56+
total_written += can_write_count;
57+
std::cout << "Producer: Copied " << can_write_count << " items. ("
58+
<< total_written << "/" << source_data.size() << " total)\n";
59+
}
60+
else {
61+
std::this_thread::yield();
62+
}
63+
}
64+
65+
std::cout << "Producer: Finished.\n";
66+
}
67+
68+
// --- Consumer Thread ---
69+
void consumer_thread(LockFreeSpscQueue<int>& queue, std::atomic<bool>& producer_is_done)
70+
{
71+
std::cout << "Consumer: Waiting for items...\n";
72+
while (true)
73+
{
74+
auto read_scope = queue.prepare_read(32);
75+
// Check if the scope is valid.
76+
if (read_scope.get_items_read() > 0)
77+
{
78+
std::cout << "Consumer: Reading a batch of " << read_scope.get_items_read() << " items...\n";
79+
for (const int& item : read_scope) {
80+
std::cout << "Consumer: Got " << item << "\n";
81+
}
82+
}
83+
else
84+
{
85+
if (producer_is_done.load(std::memory_order_acquire)) {
86+
if (queue.get_num_items_ready() == 0) {
87+
break;
88+
}
89+
} else {
90+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
91+
}
92+
}
93+
}
94+
std::cout << "Consumer: Finished.\n";
95+
}
96+
97+
98+
int main()
99+
{
100+
const size_t QUEUE_CAPACITY = 128;
101+
std::vector<int> shared_data_buffer(QUEUE_CAPACITY);
102+
LockFreeSpscQueue<int> queue(shared_data_buffer);
103+
std::atomic<bool> producer_is_done = false;
104+
105+
std::jthread producer(producer_thread, std::ref(queue));
106+
std::jthread consumer(consumer_thread, std::ref(queue), std::ref(producer_is_done));
107+
108+
producer.join();
109+
producer_is_done.store(true, std::memory_order_release);
110+
}
111+

include/LockFreeSpscQueue.h

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,81 @@ class LockFreeSpscQueue
6262
* @brief An RAII scope object representing a prepared write operation.
6363
* @details Provides direct `std::span` access to the writable blocks in the
6464
* underlying buffer. The transaction is committed when this object
65-
* is destroyed. It is move-only.
65+
* is destroyed. This object also satisfies the `std::ranges::range`
66+
* concept, allowing for direct iteration over the writable slots.
67+
* It is move-only.
6668
* @warning The user MUST write a number of items exactly equal to the value
6769
* returned by `get_items_written()`. Failure to do so will result
6870
* in the consumer reading uninitialized/garbage data.
6971
*/
7072
struct WriteScope
7173
{
74+
// --- Custom Iterator for WriteScope ---
75+
class iterator
76+
{
77+
public:
78+
using iterator_category = std::forward_iterator_tag;
79+
using value_type = T;
80+
using difference_type = std::ptrdiff_t;
81+
using pointer = T*;
82+
using reference = T&;
83+
using SpanIterator = typename std::span<T>::iterator;
84+
85+
iterator() = default;
86+
reference operator*() const { return *m_current_iter; }
87+
pointer operator->() const { return &(*m_current_iter); }
88+
89+
iterator& operator++() {
90+
++m_current_iter;
91+
if (m_in_block1 && m_current_iter == m_block1_end) {
92+
m_current_iter = m_block2_begin;
93+
m_in_block1 = false;
94+
}
95+
return *this;
96+
}
97+
iterator operator++(int) { iterator tmp = *this; ++(*this); return tmp; }
98+
bool operator==(const iterator& other) const = default;
99+
100+
private:
101+
friend struct WriteScope;
102+
iterator(SpanIterator b1_begin, SpanIterator b1_end,
103+
SpanIterator b2_begin, SpanIterator b2_end,
104+
bool is_begin)
105+
: m_block1_end(b1_end), m_block2_begin(b2_begin), m_block2_end(b2_end)
106+
{
107+
if (is_begin) {
108+
if (b1_begin != b1_end) { // If block1 is not empty, start there.
109+
m_current_iter = b1_begin;
110+
m_in_block1 = true;
111+
} else { // Otherwise, start at block2.
112+
m_current_iter = b2_begin;
113+
m_in_block1 = false;
114+
}
115+
} else { // This is the end() sentinel iterator.
116+
m_current_iter = m_block2_end; // The end is always the end of block2.
117+
m_in_block1 = false;
118+
}
119+
}
120+
121+
SpanIterator m_current_iter;
122+
SpanIterator m_block1_end;
123+
SpanIterator m_block2_begin;
124+
SpanIterator m_block2_end;
125+
bool m_in_block1 = false;
126+
};
127+
128+
// --- Making WriteScope a C++20 Range ---
129+
[[nodiscard]] iterator begin() {
130+
auto b1 = get_block1();
131+
auto b2 = get_block2();
132+
return iterator(b1.begin(), b1.end(), b2.begin(), b2.end(), true);
133+
}
134+
[[nodiscard]] iterator end() {
135+
auto b1 = get_block1();
136+
auto b2 = get_block2();
137+
return iterator(b1.begin(), b1.end(), b2.begin(), b2.end(), false);
138+
}
139+
72140
/** @brief Returns a span representing the first contiguous block to write to. */
73141
[[nodiscard]] std::span<T> get_block1() const
74142
{
@@ -103,9 +171,9 @@ class LockFreeSpscQueue
103171
WriteScope& operator=(const WriteScope&) = delete;
104172

105173
WriteScope(WriteScope&& other) noexcept
106-
: start_index1(other.start_index1), block_size1(other.block_size1),
107-
start_index2(other.start_index2), block_size2(other.block_size2),
108-
m_owner_queue(other.m_owner_queue)
174+
: start_index1(other.start_index1), block_size1(other.block_size1)
175+
, start_index2(other.start_index2), block_size2(other.block_size2)
176+
, m_owner_queue(other.m_owner_queue)
109177
{ other.m_owner_queue = nullptr; }
110178

111179
// Move assignment is deleted. It is not possible to assign to the const members,
@@ -131,13 +199,80 @@ class LockFreeSpscQueue
131199
* @brief An RAII scope object representing a prepared read operation.
132200
* @details Provides direct `std::span` access to the readable blocks in the
133201
* underlying buffer. The transaction is committed when this object
134-
* is destroyed. It is move-only.
202+
* is destroyed. This object also satisfies the `std::ranges::range`
203+
* concept, allowing direct iteration. It is move-only.
135204
* @warning The user MUST treat all data within the returned spans as read.
136205
* The full `get_items_read()` amount will be committed, advancing
137206
* the read pointer and making the space available for future writes.
138207
*/
139208
struct ReadScope
140209
{
210+
// --- Custom Iterator for ReadScope ---
211+
class iterator
212+
{
213+
public:
214+
using iterator_category = std::forward_iterator_tag;
215+
using value_type = const T;
216+
using difference_type = std::ptrdiff_t;
217+
using pointer = const T*;
218+
using reference = const T&;
219+
using SpanConstIterator = typename std::span<const T>::iterator;
220+
221+
iterator() = default;
222+
reference operator*() const { return *m_current_iter; }
223+
pointer operator->() const { return &(*m_current_iter); }
224+
225+
iterator& operator++() {
226+
++m_current_iter;
227+
if (m_in_block1 && m_current_iter == m_block1_end) {
228+
m_current_iter = m_block2_begin;
229+
m_in_block1 = false;
230+
}
231+
return *this;
232+
}
233+
iterator operator++(int) { iterator tmp = *this; ++(*this); return tmp; }
234+
bool operator==(const iterator& other) const = default;
235+
236+
private:
237+
friend struct ReadScope;
238+
iterator(SpanConstIterator b1_begin, SpanConstIterator b1_end,
239+
SpanConstIterator b2_begin, SpanConstIterator b2_end,
240+
bool is_begin)
241+
: m_block1_end(b1_end), m_block2_begin(b2_begin), m_block2_end(b2_end)
242+
{
243+
if (is_begin) {
244+
if (b1_begin != b1_end) {
245+
m_current_iter = b1_begin;
246+
m_in_block1 = true;
247+
} else {
248+
m_current_iter = b2_begin;
249+
m_in_block1 = false;
250+
}
251+
} else {
252+
m_current_iter = m_block2_end;
253+
m_in_block1 = false;
254+
}
255+
}
256+
257+
SpanConstIterator m_current_iter;
258+
SpanConstIterator m_block1_end;
259+
SpanConstIterator m_block2_begin;
260+
SpanConstIterator m_block2_end;
261+
bool m_in_block1 = false;
262+
};
263+
264+
// --- Making ReadScope a C++20 Range ---
265+
[[nodiscard]] iterator begin() const {
266+
auto b1 = get_block1();
267+
auto b2 = get_block2();
268+
return iterator(b1.begin(), b1.end(), b2.begin(), b2.end(), true);
269+
}
270+
[[nodiscard]] iterator end() const {
271+
auto b1 = get_block1();
272+
auto b2 = get_block2();
273+
return iterator(b1.begin(), b1.end(), b2.begin(), b2.end(), false);
274+
}
275+
141276
/** @brief Returns a span representing the first contiguous block to read from. */
142277
[[nodiscard]] std::span<const T> get_block1() const
143278
{

0 commit comments

Comments
 (0)