Inspired by the "bip buffer", I coded a fixed-size circular queue that uses two regions (two pairs of begin/end pointers) to account for the wrap-around.
The main goal of this design was to simplify the queue operations implementation, and in the case of cqueue_size
, cqueue_empty
and cqueue_full
it happened.
However, cqueue_pop
and cqueue_push
are messy, and I am concerned that a bug may be hiding in those functions.
#pragma once
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
// WARN: these macros are meant for internal use exclusively!
#define BUFFER_SIZE 8
#define BUFFER_BEGIN(pcq) ((pcq)->buffer)
#define BUFFER_END(pcq) ((pcq)->buffer + BUFFER_SIZE)
#define REGIONA_SIZE(pcq) ((pcq)->ra_end - (pcq)->ra_begin)
#define REGIONB_SIZE(pcq) ((pcq)->rb_end - (pcq)->rb_begin)
#define REGIONB_ACTIVATE(pcq) \
if ((pcq)->rb_begin == NULL && (pcq)->rb_end == NULL) { \
(pcq)->rb_begin = BUFFER_BEGIN(pcq); \
(pcq)->rb_end = BUFFER_BEGIN(pcq); \
} else (void)0
#define REGIONB_DEACTIVATE(pcq) if (true) { \
(pcq)->rb_begin = NULL; \
(pcq)->rb_end = NULL; \
} else (void)0
///
/// @brief Circular queue.
/// @details Circular queue with fixed capacity, with design inspired by the "bip buffer" (bipartite buffer).
/// This implementation attempts to simplify the code of the circular queue operations by
/// abstracting away the wrap-around as two regions in the internal buffer: RegionA and RegionB.
/// RegionA can begin anywhere in the InternalBuffer but only end at the InternalBuffer's end.
/// RegionB can only begin at the beginning of the InternalBuffer.
/// RegionB can only exist if RegionA has reached the end of the InternalBuffer (i.e. RegionA is full).
/// RegionB becomes the new RegionA when the old RegionA becomes empty.
/// @remarks At the cost of keeping two extra pointers for RegionB, the pseudocode is simplified thusly:
/// @remarks CQUEUE_EMPTY: RegionA.begin == RegionA.end
/// @remarks CQUEUE_FULL: RegionA.begin == RegionB.end || RegionA.size == InternalBuffer.size
/// @remarks CQUEUE_SIZE: (RegionA.end - RegionA.begin) + (RegionB.end - RegionB.begin)
///
typedef struct
{
char buffer[BUFFER_SIZE]; ///< The circular queue's byte buffer.
char *ra_begin; ///< RegionA beginning.
char *ra_end; ///< RegionA end.
char *rb_begin; ///< RegionB beginning.
char *rb_end; ///< RegionB end.
} cqueue_t;
// circular queue operations
static inline void cqueue_reset(cqueue_t *cq);
static inline char cqueue_pop(cqueue_t *cq);
static inline void cqueue_push(cqueue_t *cq, char c);
static inline size_t cqueue_size(const cqueue_t *cq);
static inline bool cqueue_empty(const cqueue_t *cq);
static inline bool cqueue_full(const cqueue_t *cq);
///
/// @brief Resets the circular queue `cq` to a default empty state.
/// @note The circular queue's internal buffer is cleared to 0.
/// @param [out] cq Pointer to the circular queue to be reset.
///
static inline void cqueue_reset(cqueue_t *cq)
{
memset(BUFFER_BEGIN(cq), 0, BUFFER_SIZE);
cq->ra_begin = BUFFER_BEGIN(cq);
cq->ra_end = BUFFER_BEGIN(cq);
REGIONB_DEACTIVATE(cq); // RegionB doesn't exist unless RegionA is full
}
///
/// @brief Pops the next byte from the circular queue `cq`.
/// @warning If the circular queue is empty, a spurious value of 0 will be "popped".
/// @param [in,out] cq Pointer to the circular queue to be changed.
/// @returns The value of the current top byte.
/// @retval 0 If the circular queue is empty.
///
static inline char cqueue_pop(cqueue_t *cq)
{
if (REGIONA_SIZE(cq) != 0) // does RegionA have some data?
{
// retrieve value of popped byte
const char r = *(cq->ra_begin)++;
if (REGIONA_SIZE(cq) == 0 && REGIONB_SIZE(cq) != 0) // is RegionA empty while RegionB isn't?
{
// RegionB becomes RegionA
cq->ra_begin = cq->rb_begin;
cq->ra_end = cq->rb_end;
REGIONB_DEACTIVATE(cq);
}
return r;
}
else // the circular queue is empty
return 0;
}
///
/// @brief Pushes the new byte `c` to the circular queue `cq`.
/// @warning If the circular queue is full, the new byte will overwrite the top byte.
/// @param [in,out] cq Pointer to the circular queue to be changed.
/// @param c Byte to be pushed.
///
static inline void cqueue_push(cqueue_t *cq, char c)
{
if (cqueue_full(cq)) // is the circular queue full?
{
// push to RegionB and overwrite the current top byte in RegionA
REGIONB_ACTIVATE(cq);
*(cq->rb_end)++ = c;
++cq->ra_begin;
if (cq->ra_begin == BUFFER_END(cq)) // is RegionA empty?
{
// RegionB becomes RegionA
cq->ra_begin = cq->rb_begin;
REGIONB_DEACTIVATE(cq);
}
}
else
if (cq->ra_end == BUFFER_END(cq)) // is RegionA full?
{
// push to RegionB
REGIONB_ACTIVATE(cq);
*(cq->rb_end)++ = c;
}
else // RegionA is not full
{
// push to RegionA
*(cq->ra_end)++ = c;
}
}
///
/// @brief Returns the size of the circular queue `cq`.
/// @param [in] cq Pointer to the circular queue to be analyzed.
/// @returns The size in bytes of the circular queue.
///
static inline size_t cqueue_size(const cqueue_t *cq)
{
return REGIONA_SIZE(cq) + REGIONB_SIZE(cq);
}
///
/// @brief Returns whether or not the circular queue `cq` is empty.
/// @param [in] cq Pointer to the circular queue to be analyzed.
/// @retval true If the circular queue is empty.
/// @retval false If the circular queue is not empty.
///
static inline bool cqueue_empty(const cqueue_t *cq)
{
// NOTE: if RegionA doesn't exist, neither does RegionB;
// this is why it's enough to check only the size of RegionA
// to determine if the circular queue is empty
return REGIONA_SIZE(cq) == 0;
}
///
/// @brief Returns whether or not the circular queue `cq` is full.
/// @param [in] cq Pointer to the circular queue to be analyzed.
/// @retval true If the circular queue is full.
/// @retval false If the circular queue is not full.
///
static inline bool cqueue_full(const cqueue_t *cq)
{
return cq->ra_begin == cq->rb_end || REGIONA_SIZE(cq) == BUFFER_SIZE;
}