What would be the preferred C way of implementing a simple generic ring-buffer.
Which approach from the 2 below (or even some 3rd) would you use and why?
Specifically, this will be part of an embedded automotive application, which should conform to MISRA C:2012. I am looking for an approach that is the easiest to understand and maintain. Performance is not a deciding factor.
Option 1, having common structure and using void pointers:
typedef struct ring_buffer
{
void *start_of_buffer; // inclusive
void *end_of_buffer; // exclusive
size_t element_size;
void *head;
void *tail;
size_t count;
} ring_buffer_t;
void * next_pointer(ring_buffer_t *handle, void *pointer)
{
void *next = (char *)pointer + handle->element_size;
if (next == handle->end_of_buffer)
next = handle->start_of_buffer;
return next;
}
void push(ring_buffer_t *handle, void *element)
{
memcpy(handle->head, element, handle->element_size);
handle->head = next_pointer(handle, handle->head);
handle-count++;
}
void pop(ring_buffer_t *handle, void *element)
{
memcpy(element, handle->tail, handle->element_size);
handle->tail = next_pointer(handle, handle->tail);
handle->count--;
}
Option 2, generating type specific structure and using macros:
#define ring_buffer_t(type) \
struct \
{ \
type *start_of_buffer; \
type *end_of_buffer; \
type *head; \
type *tail; \
size_t count; \
}
#define NEXT_POINTER(handle, pointer) \
(pointer == (handle.end_of_buffer - 1)) ? start_of_buffer : pointer + 1
#define PUSH(handle, element) \
do \
{ \
*handle.head = element; \
handle.head = NEXT_POINTER(handle, handle.head); \
handle.count++; \
} while (0)
#define POP(handle, element) \
do \
{ \
element = *handle.tail; \
handle.tail = NEXT_POINTER(handle, handle.tail); \
handle.count--; \
} while (0)
I am leaning more towards option 2 because:
- this is a simple data structure with very simple API and not much logic in push and pop functions
- option 1 includes some overhead having to store element size and then increment pointers based on this size
- using clear variable assignment instead of memcpy() as in option 1
- type-safe pushing and popping as opposed to option 1
What I dislike about option 2:
- macros complicating maintainability and possibly making it hard for new developers to understand (although in this case I think it is pretty straightforward)
- macros making it more difficult to debug (although not much to debug)
- multiple statements in push and pop macros (although only 3)