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; }