Circular buffer Created 2026-10-13 Updated 2026-10-18
A circular buffer stores a changing sequence in a fixed-capacity array, reusing slots by wrapping array indices through modular arithmetic. A front array index and an occupied count can represent a FIFO queue without shifting entries after removal. The physical array stays in place; the position at which its logical sequence begins moves. This makes a circular buffer useful when storage must remain bounded while successive items arrive and leave.
Capacity and overflow behavior are separate choices. A rejecting FIFO queue preserves all accepted items until removal and reports failure on a full insertion. An overwriting circular buffer deliberately discards its oldest item to retain the newest arrivals. These are different application programming interfaces. The existing representation independence example specifies the rejecting policy and gives its abstraction function and correctness argument against a shifting array implementation.
Circular buffer full and empty states 2026-10-18
With front-plus-count storage, empty means and full means ; every allocated slot is usable. With only the two wrapped array indices , the equality describes both possibilities. Indeed , and the two allowed counts have the same remainder. Recovering the count as therefore loses the full state. A count, a separate full flag, or a different index representation must resolve that ambiguity.
Another convention reserves one slot: for a physical array of length , empty means and full means . Its usable capacity is , and now recovers its count because . A five-slot array then stores at most four items.
This is the convention of the kernel's documented
CIRC_* helpers, which additionally require power-of-two array lengths. It is not a universal property of circular buffers; our capacity-five count-based example uses all five slots and does not use those helpers.[ref]Circular Buffers by David Howells and Paul E. McKenney. David Howells and Paul E. McKenney, Circular Buffers, What is a circular buffer?; Measuring power-of-2 buffers: full/empty convention and helper result range
Circular buffer indices Created 2026-10-18 Updated 2026-10-24
Let the capacity be a positive integer , and allocate exactly array slots . Store the front array index and count . The representation invariant isThe logical sequence and next-write array index areHere denotes the nonnegative remainder from modular arithmetic. The count determines which slots are live: leftover bits in other slots are not members of the FIFO queue. If , has no current front item; it identifies the position where the next successful insertion begins. If , points at a live item, so writing there requires an explicit overwrite policy.
For ,
enqueue(x) writes , then increments , leaving fixed. For , dequeue() saves and returns , then sets and . These are sequential updates, completed before the next call. A rejecting interface tests or first and returns full or empty, respectively, without changing state. peek returns only when ; size returns .One can instead store and , recovering . On insertion, advance and increment ; on removal, keep fixed and decrement . Thus the next-write array index is not necessarily the front array index.
Names such as
head and tail require checking the actual API: the kernel documentation calls the insertion position head and the removal position tail.[ref]Circular Buffers by David Howells and Paul E. McKenney. David Howells and Paul E. McKenney, Circular Buffers, What is a circular buffer?: head is insertion, tail is removal
The diagram is the state after step 8 of circular buffer operation trace. Reading the physical array from slot zero would give , which includes a removed label and puts first. Reading from for exactly items gives . The next insertion uses slot , not slot . This distinction also explains why the live sequence may need two contiguous pieces of the array: here slots through , followed by slot . No items need to move to make this wrap work.
Circular buffer operation costs 2026-10-18
With storage allocated in advance, each successful insertion, removal,
peek, size, and full/empty check uses worst-case time complexity in Big O notation. This assumes constant-cost indexed array access, bounded machine word index arithmetic and fixed-cost scalar item reads and writes. Each operation touches a bounded number of slots and fields regardless of and ; no shifting, scanning or resizing occurs. Rejected operations also take constant work. Producing a copy of all live items takes work under the same scalar-cost assumptions.The preallocated array occupies scalar slots plus constant bookkeeping. Allocation and any initial slot construction are separate setup costs. Copying a large item, running an expensive destructor or callback, allocating an item, or growing a dynamic array changes the operation cost and must be charged separately. The bound is worst-case structural work, with no need for amortized analysis, and says nothing about a measured wall-clock deadline.
These formulas describe sequential calls. Wrapping array indices implies neither atomic operations nor safe access from multiple software threads. Publication of an item's contents and reuse of its slot require a protocol justified under the relevant memory model, including any necessary acquire-release synchronization. The kernel documentation discusses barriers under explicit producer/consumer restrictions; Boost's documentation separately requires mutual exclusion for shared access involving writes. Neither property follows merely from choosing a circular buffer.[ref]
Circular Buffers by David Howells and Paul E. McKenney. David Howells and Paul E. McKenney, Circular Buffers, Using memory barriers with circular buffers; The producer; The Consumer: explicit synchronization and ordering requirements
[ref]Implementation. Jan Gaspar, Boost.Circular Buffer, Implementation, Thread-Safety: shared access involving a writer requires mutual exclusion
Circular buffer overflow policy 2026-10-18
Continue from step 9 with a proposed insertion of . A rejecting operation
try_enqueue(H) returns full, writes nothing and leaves with . The next dequeue() returns , leaving with . Rejection requires the caller to decide whether to retry or abandon .An overwriting operation
append_latest(H) instead writes , advances , and keeps . It can report the discarded label as part of its specified result. At step 10 this gives with . The next dequeue() returns , leaving with . Below full capacity, append_latest follows the ordinary insertion rule without discarding anything.The lower bars in the trace diagram expose the consequence: entered at step 4 and remains a member until step 11 under rejection, but only until step 10 under overwrite. never becomes a member in the rejecting branch; it does in the overwriting branch. The same physical slot can therefore hold different logical items over time. These interfaces are observably different, so representation independence cannot justify exchanging them while retaining the same promised results.
Boost's documented
push_back behavior chooses overwrite when full, implemented by assignment to the old element. Its discussion of iterator invalidation illustrates why a reused address need not still identify the original item. Assignment, object destruction, and release of an owned resource are separate language-level concerns; the membership bars make no claim about those lifetimes.[ref]Implementation. Jan Gaspar, Boost.Circular Buffer, Implementation: Overwrite Operation; Writing to a Full Buffer; Iterator Invalidation; Caveats
A rejecting FIFO queue suits work that must remain pending until explicitly removed. An overwriting circular buffer suits a bounded recent history where losing the oldest retained item is the stated policy. Neither storage wrap nor bounded capacity chooses between those requirements.
If the merge algorithm from part (b) is retained with a disjoint destination, use a circular buffer of cells. Represent the live segment by its start modulo and its length . The other cells are free; the next merge needs at most output cells. Appending its output and advancing the live start releases exactly as many obsolete source cells. Logical run boundaries and copying indices are reduced modulo . The final sorted sequence may wrap, in which case an array rotation makes it contiguous. ThusThis is also the worst-case requirement if all source cells must be retained until a whole disjoint merge has been written: the final two runs can have combined length .
Under the question's literal wording, however, no running time requirement or disjoint-output restriction is imposed on the modified implementation. The absolute smallest array isTo achieve this, replace the disjoint-output merge by an in-place merge: compare the next unread values of the two adjacent runs; when the second is smaller, save it in one scalar, shift the intervening first-run block right by one, and insert it before that block. This is a sorted merge using no extra array. Its loop invariant is that the finished prefix is the sorted merge of the consumed portions and the two remaining runs stay sorted. Each step consumes one value, proving termination.
After merging the first two runs in place, rotate the merged prefix behind the untouched suffix. Three array reversals implement this rotation with one scalar: reverse the merged prefix, reverse the suffix, then reverse the whole segment. The resulting live sequence is exactly the one produced in part (c). Repeat the same run schedule until sorted. The shift and reversal loops respectively preserve the copied block and exchanged end pairs; their loop variants are the unshifted length and half the unreversed length. The modification can be substantially slower, but preserves the loop invariant and decreasing run count from part (d). Fewer than cells cannot even hold the specified input entries. Thus is the natural circular-workspace answer for the original merge primitive, while is attainable when minimizing storage over all implementations allowed by the stated time-free specification.
A fixed-size array cannot implement unbounded successful insertion. Either specify a bounded double-ended queue with an extra not-full insertion precondition, or retain the unbounded interface and specify a possible full-capacity failure. Silently overwriting an endpoint would change the stated behavior. Here use the bounded contract, with a fixed positive capacity
MAX and abstract state . Both additions require ; the other postconditions remain unchanged. The class declaration in part (c) is interpreted as implementing this explicitly revised contract, rather than as a behavioral subtype of the unrestricted contract in part (a).The concrete state is an array
a of length MAX, a first-position index head, and an occupied-entry count size. Its representation invariant isThe abstraction function is for . Unoccupied slots are unconstrained. Storing size distinguishes empty and full states even though their two circular endpoint positions coincide. MAX=1 is valid. Using a circular buffer avoids shifting entries and makes every endpoint operation constant time.Maintain the latest five temperature readings in a circular buffer, together with their running sum. The following C programming language code spells out the message passing interface rather than assuming a particular library:
send(dst,&m) is a blocking synchronous message passing send; receive(src,&m) receives from the specified sender or ANY and returns the sender's identity; sendrec(dst,&m) atomically sends the request and waits for that sender's reply in the same message object. Successful, reliable operations are assumed. Message contents are copied by the interface.enum { ANY = -1, TIMER = 1, TEMP = 2 };
enum { PULSE, PING, REQUEST, REPLY };
typedef struct {
int type;
int period_ms;
double value;
} Message;
extern void send(int dst, const Message *m);
extern int receive(int src, Message *m);
extern void sendrec(int dst, Message *m);
void smooth(void) {
double history[5] = {0};
double sum = 0;
int count = 0, next = 0;
Message m = { .type = PULSE, .period_ms = 1000 };
send(TIMER, &m); /* subscribe once */
for (;;) {
int sender = receive(count < 5 ? TIMER : ANY, &m);
if (sender == TIMER && m.type == PING) {
Message t = { .type = REQUEST };
sendrec(TEMP, &t); /* matching REPLY contains t.value */
if (count == 5)
sum -= history[next];
else
++count;
history[next] = t.value;
sum += t.value;
next = (next + 1) % 5;
} else if (count == 5 && m.type == REQUEST) {
Message answer = { .type = REPLY, .value = sum / 5.0 };
send(sender, &answer);
}
}
}Before the fifth reading, selective reception from
TIMER prevents premature client requests from being accepted. Such clients wait at their synchronous sends; they are not given a partial arithmetic mean. Thereafter the operating-system process receives both timer messages and client requests. Each accepted PING initiates one measurement; each accepted client request returns the arithmetic mean of the five most recently completed measurements, without initiating another measurement or changing the circular buffer.The loop invariant is that
count is the number of valid readings, capped at five, sum is their sum, and next is the next unused slot during startup or the oldest slot afterwards. Removing the overwritten reading and adding the new one preserves this loop invariant. Each update and reply uses time complexity and storage, since the window length is fixed. Under timely sensor replies, timer delivery and process scheduling, measurements are initiated once per second. The next parts explain failures of these timing assumptions; the code does not promise one-second sampling when the sensor itself cannot sustain it. Representation independence 2026-10-13
Representation independence means that clients restricted to the promised API cannot distinguish implementations by the specified results and state transitions. State what observations count: equal returned labels and failure statuses do not imply equal running times, allocations or physical layouts. If the API also promises a resource bound, a replacement must meet that bound too.
Replace the compact array in the bounded queue with encapsulation by a circular buffer with storage , front index and count . Its representation invariant is , , and . Its abstraction function reads the occupied slots using modular arithmetic:On successful
enqueue(x), write and increment . On successful dequeue(), return , replace by and decrement . Full and empty failures do not alter storage. peek and size have the same abstract meanings as before. Keeping a count distinguishes a full circular buffer from an empty one even when both would have the same next insertion index.For , start empty, enqueue , dequeue twice, enqueue , try to enqueue , then
peek and size. Both implementations return from the removals, report full for , and finish with front , size three and abstract sequence . The compact array stores from index zero; the circular buffer stores with . Their abstraction functions explain why different stored entries describe the same ordered content.Under constant-cost slot access and index arithmetic, with no resizing and labels that fit in one slot, the compact array does slot moves on a successful removal: worst-case time complexity as capacity varies. The circular buffer does no slot moves and uses worst-case time complexity for all four operations. Both allocate slots initially; that construction cost is separate. These are operation-count claims in Big O notation, not measured timings. The short correctness outline checks that each implementation's abstraction function produces the specified new sequence and return value after every successful or rejected call. Repeating that check gives equal traces for clients using only this API.

