969 Circular Buffer
Tutorial
The Problem
Implement a fixed-capacity circular ring buffer (FIFO) where pushing to a full buffer overwrites the oldest element. Use separate head, tail, and count indices with modular arithmetic. Unlike VecDeque (which grows), this ring buffer has a fixed capacity — modeling hardware FIFOs, event log windows, and streaming buffers.
🎯 Learning Outcomes
RingBuffer<T: Default + Clone> with Vec<T> and head/tail/count trackingtail, advance tail = (tail + 1) % capacity, and when full advance head to overwrite oldesthead, advance head = (head + 1) % capacity, decrement countis_full, is_empty, size, and peek without special-casing the wrap-aroundT: Default is needed for pre-allocation: vec![T::default(); capacity]Code Example
#![allow(clippy::all)]
// 969: Circular / Ring Buffer
// Fixed-capacity FIFO: push overwrites oldest when full
// OCaml: array + head/tail/count refs; Rust: Vec + indices
pub struct RingBuffer<T> {
data: Vec<T>,
capacity: usize,
head: usize, // next read index
tail: usize, // next write index
count: usize,
}
impl<T: Default + Clone> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "capacity must be > 0");
RingBuffer {
data: vec![T::default(); capacity],
capacity,
head: 0,
tail: 0,
count: 0,
}
}
}
impl<T: Clone> RingBuffer<T> {
pub fn is_full(&self) -> bool {
self.count == self.capacity
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn size(&self) -> usize {
self.count
}
/// Push: if full, overwrites oldest element
pub fn push(&mut self, x: T) {
self.data[self.tail] = x;
self.tail = (self.tail + 1) % self.capacity;
if self.is_full() {
// Overwrite: advance head (drop oldest)
self.head = (self.head + 1) % self.capacity;
} else {
self.count += 1;
}
}
/// Pop: removes and returns oldest element
pub fn pop(&mut self) -> Option<T> {
if self.is_empty() {
None
} else {
let x = self.data[self.head].clone();
self.head = (self.head + 1) % self.capacity;
self.count -= 1;
Some(x)
}
}
/// Peek: view oldest without removing
pub fn peek(&self) -> Option<&T> {
if self.is_empty() {
None
} else {
Some(&self.data[self.head])
}
}
/// Collect all elements in order (oldest first)
pub fn to_vec(&self) -> Vec<T> {
(0..self.count)
.map(|i| self.data[(self.head + i) % self.capacity].clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_push_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
assert!(r.is_empty());
r.push(1);
r.push(2);
r.push(3);
r.push(4);
assert!(r.is_full());
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&1));
}
#[test]
fn test_overwrite_on_full() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
r.push(1);
r.push(2);
r.push(3);
r.push(4);
r.push(5); // overwrites 1
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&2));
assert_eq!(r.to_vec(), vec![2, 3, 4, 5]);
}
#[test]
fn test_pop_order() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
assert_eq!(r.pop(), Some(1));
assert_eq!(r.pop(), Some(2));
assert_eq!(r.size(), 2);
}
#[test]
fn test_wrap_around() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
r.pop();
r.pop();
r.push(5);
r.push(6);
r.push(7); // overwrite
assert_eq!(r.to_vec(), vec![4, 5, 6, 7]);
}
#[test]
fn test_empty_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(2);
assert_eq!(r.pop(), None);
assert_eq!(r.peek(), None);
}
}Key Differences
| Aspect | Rust | OCaml |
|---|---|---|
| Pre-allocation | vec![T::default(); capacity] | Array.make capacity default (explicit default) |
| Mutable fields | let mut struct fields | mutable record fields |
| Modular wrap | % capacity | mod capacity |
| Return on pop | Option<T> via Some(x.clone()) | 'a option via Some x |
The circular buffer is a fixed-size sliding window over a stream of values. It is the foundation of audio/video buffers, network packet queues, and producer-consumer pipelines where only the most recent n items matter.
OCaml Approach
type 'a ring_buffer = {
data: 'a array;
capacity: int;
mutable head: int;
mutable tail: int;
mutable count: int;
}
let create capacity default =
{ data = Array.make capacity default;
capacity; head = 0; tail = 0; count = 0 }
let push buf x =
buf.data.(buf.tail) <- x;
buf.tail <- (buf.tail + 1) mod buf.capacity;
if buf.count = buf.capacity
then buf.head <- (buf.head + 1) mod buf.capacity
else buf.count <- buf.count + 1
let pop buf =
if buf.count = 0 then None
else begin
let x = buf.data.(buf.head) in
buf.head <- (buf.head + 1) mod buf.capacity;
buf.count <- buf.count - 1;
Some x
end
OCaml's mutable record fields allow in-place update. The algorithm is identical: % capacity wraps both indices, and push on a full buffer advances head to drop the oldest entry.
Full Source
#![allow(clippy::all)]
// 969: Circular / Ring Buffer
// Fixed-capacity FIFO: push overwrites oldest when full
// OCaml: array + head/tail/count refs; Rust: Vec + indices
pub struct RingBuffer<T> {
data: Vec<T>,
capacity: usize,
head: usize, // next read index
tail: usize, // next write index
count: usize,
}
impl<T: Default + Clone> RingBuffer<T> {
pub fn new(capacity: usize) -> Self {
assert!(capacity > 0, "capacity must be > 0");
RingBuffer {
data: vec![T::default(); capacity],
capacity,
head: 0,
tail: 0,
count: 0,
}
}
}
impl<T: Clone> RingBuffer<T> {
pub fn is_full(&self) -> bool {
self.count == self.capacity
}
pub fn is_empty(&self) -> bool {
self.count == 0
}
pub fn size(&self) -> usize {
self.count
}
/// Push: if full, overwrites oldest element
pub fn push(&mut self, x: T) {
self.data[self.tail] = x;
self.tail = (self.tail + 1) % self.capacity;
if self.is_full() {
// Overwrite: advance head (drop oldest)
self.head = (self.head + 1) % self.capacity;
} else {
self.count += 1;
}
}
/// Pop: removes and returns oldest element
pub fn pop(&mut self) -> Option<T> {
if self.is_empty() {
None
} else {
let x = self.data[self.head].clone();
self.head = (self.head + 1) % self.capacity;
self.count -= 1;
Some(x)
}
}
/// Peek: view oldest without removing
pub fn peek(&self) -> Option<&T> {
if self.is_empty() {
None
} else {
Some(&self.data[self.head])
}
}
/// Collect all elements in order (oldest first)
pub fn to_vec(&self) -> Vec<T> {
(0..self.count)
.map(|i| self.data[(self.head + i) % self.capacity].clone())
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_push_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
assert!(r.is_empty());
r.push(1);
r.push(2);
r.push(3);
r.push(4);
assert!(r.is_full());
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&1));
}
#[test]
fn test_overwrite_on_full() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
r.push(1);
r.push(2);
r.push(3);
r.push(4);
r.push(5); // overwrites 1
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&2));
assert_eq!(r.to_vec(), vec![2, 3, 4, 5]);
}
#[test]
fn test_pop_order() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
assert_eq!(r.pop(), Some(1));
assert_eq!(r.pop(), Some(2));
assert_eq!(r.size(), 2);
}
#[test]
fn test_wrap_around() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
r.pop();
r.pop();
r.push(5);
r.push(6);
r.push(7); // overwrite
assert_eq!(r.to_vec(), vec![4, 5, 6, 7]);
}
#[test]
fn test_empty_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(2);
assert_eq!(r.pop(), None);
assert_eq!(r.peek(), None);
}
}#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_push_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
assert!(r.is_empty());
r.push(1);
r.push(2);
r.push(3);
r.push(4);
assert!(r.is_full());
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&1));
}
#[test]
fn test_overwrite_on_full() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
r.push(1);
r.push(2);
r.push(3);
r.push(4);
r.push(5); // overwrites 1
assert_eq!(r.size(), 4);
assert_eq!(r.peek(), Some(&2));
assert_eq!(r.to_vec(), vec![2, 3, 4, 5]);
}
#[test]
fn test_pop_order() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
assert_eq!(r.pop(), Some(1));
assert_eq!(r.pop(), Some(2));
assert_eq!(r.size(), 2);
}
#[test]
fn test_wrap_around() {
let mut r: RingBuffer<i32> = RingBuffer::new(4);
for i in 1..=4 {
r.push(i);
}
r.pop();
r.pop();
r.push(5);
r.push(6);
r.push(7); // overwrite
assert_eq!(r.to_vec(), vec![4, 5, 6, 7]);
}
#[test]
fn test_empty_pop() {
let mut r: RingBuffer<i32> = RingBuffer::new(2);
assert_eq!(r.pop(), None);
assert_eq!(r.peek(), None);
}
}
Deep Comparison
Circular Buffer — Comparison
Core Insight
A ring buffer uses modular arithmetic (i = (i + 1) % capacity) to wrap around array indices, creating the illusion of a circular structure from a linear array. Three indices (head, tail, count) fully describe state. Both OCaml and Rust implement this identically; the main difference is OCaml uses mutable record fields vs Rust's let mut struct fields.
OCaml Approach
mutable head/tail/count in a recordArray.make capacity default — requires a default valuer.head <- (r.head + 1) mod r.capacity — modular advancecount fieldRust Approach
head: usize, tail: usize, count: usizevec![T::default(); capacity] — requires T: Default + Cloneself.tail = (self.tail + 1) % self.capacity — same modular advanceis_full() before incrementing count (slightly different branch order)to_vec() for snapshot: (0..count).map(|i| data[(head+i)%cap]) Comparison Table
| Aspect | OCaml | Rust |
|---|---|---|
| State | Mutable record fields | &mut self struct |
| Array init | Array.make cap default | vec![T::default(); cap] |
| Modular advance | (i + 1) mod capacity | (i + 1) % capacity |
| Full check | count = capacity | count == capacity |
| Overwrite | Advance both head and tail | Advance head after tail write |
| Snapshot | Manual fold | .map(|i| data[(head+i)%cap]) |
| Generic | 'a ring | RingBuffer<T> with T: Clone |
Exercises
to_vec(&self) -> Vec<T> that returns elements in FIFO order (oldest first).iter(&self) -> impl Iterator<Item=&T> that traverses from head to tail.push_no_overwrite variant that returns Err instead of overwriting when full.Mutex<RingBuffer<T>> and benchmark it vs crossbeam's bounded channel.