Generic Circular Queue / Circular Buffer / Ring buffer in C++
Circular queue / Circular buffer / Ring buffer:
Introduction:
The intention of this design to provide the generic circular buffer which can be used for any data types.
What is circular buffer ?
Circular buffer/queue is a liner data structure which has fixed size of array where both the ends are connected. This data structure works as FIFO(First In First Out) model. That means old data is read first. If the buffer is full or reached max size, it will start overwriting the old data from the beginning.
Circular/ring buffer is used in producer consumer problem, where consumer is not meeting the speed of the producer, Producer produces the new data and overwrite with the old data , if the buffer reached maximum size.
Circular buffer is the good choice for implementing queue that has fixed maximum size. All the operation happens at constant time (push, pop).
Implementation:
Source code : sourcecode/circularbuffer.h
Producer consumer program with circular queue: multithreading.h
Class declaration:
Used template to provide the generic circular buffer.
//Circular buffer
template<typename T>
class CCircularBuffer
{
//static pointer
static CCircularBuffer* circularBuffer; //singleton instance of the class. This makes all the clients points to the same object.
//inline declration available from C++17
//static CCircularBuffer* circularBuffer = nullptr;
//data pointer
shared_ptr<T[]> bufferData; //declared as shared ptr , so that when all the clients deferences, pointer will be deleted desturctor of the shared ptr.
uint8_t maxSize; //maximum size of the buffer
uint8_t front; //write index
uint8_t back; //read index
uint8_t bufferSize; //size of the buffer
public:
//function to get the object of the circular buffer
static CCircularBuffer* getCircularBuffer();
//constructor
CCircularBuffer();
//Push the element
void push(T data);
//Get the old/first data
T pop();
//Get the number of elements in the buffer
uint8_t getSize();
//check any data is in buffer
bool isEmpty();
//check the full status of the buffer
bool isFull();
//utility function to print the elements in the buffer
void printBuffer();
};
Any variable type, user defined data type can be stored in the circular buffer. For example,
CCircularBuffer<int>* cBuffer = CCircularBuffer<int>::getCircularBuffer();
CCircularBuffer<float>* cBuffer = CCircularBuffer<float>::getCircularBuffer();
CCircularBuffer<char>* cBuffer = CCircularBuffer<char>::getCircularBuffer();
Static instance:
Provided static instance, so that all the client will have the same object.
Each instantiation of class template has its own copy of member static variables.
//static pointer
static CCircularBuffer* circularBuffer;
//static variable declaration
template<typename T> CCircularBuffer<T>* CCircularBuffer<T>::circularBuffer = 0;
Buffer:
bufferData declared as shared pointer. so that when all the clients deference, then pointer will get released by the shared pointer destructor.
For example, producer consumer program where producer and consumer access the same buffer.
shared_ptr<T[]> bufferData;
//create the memory
bufferData = shared_ptr<T[]> (new T[maxSize]);
Singleton object instance:
//function to get the object of the circular buffer
static CCircularBuffer* getCircularBuffer()
{
if(0 == circularBuffer)
{
circularBuffer = new CCircularBuffer();
}
return circularBuffer;
}
Push the data to circular buffer:
There are two index provided in this class. 1. front 2. back.
back index is used for pushing the data inside the circular buffer.
If the buffer size reaches maximum size of the buffer, then start overwrite the data from the beginning (index 0).
Also, reset the front index to back. So that old content is read first.
if(isFull())
{
back = (back+1) % maxSize;
//change the front index value when the buffer reaches MAX size.
//It reset the front index to read from the most old data.
front = back;
}
Note: In circular buffer, use % modulo operator while incrementing the index value, this will provide the index
value between the 0 to maxsize of the buffer.
//Push the element
void push(T data)
{
//data push always happens at back index
bufferData[back] = data;
//if the bufferSize reaches the max, it stays there.
if(bufferSize < maxSize)
{
bufferSize++;
}
//when max size reached, reset the back index to beginning.
if(isFull())
{
back = (back+1) % maxSize;
//change the front index value when the buffer reaches MAX size.
//It reset the front index to read from the most old data.
front = back;
}
else
{
//increment the back index
back = (back+1) % maxSize;
}
}
Pop the data from the circular buffer:
Used front index to read the data from the buffer.
//Get the old/first data
T pop()
{
//No data available then return -1.
if(isEmpty())
return -1;
//get the most old data using front index.
T data = bufferData[front];
//increment the front index
front = (front+1) % maxSize;
//decrease the bufferSize only if the size greater than zero
if(bufferSize > 0)
{
bufferSize--;
}
return data;
}
Size:
bufferSize variable is used to find the number of elements stored in the buffer.
This value is modified during the push and pop operation.
Note: This value remains between 0 to maximum size of the buffer.
Utility function:
//utility function to print the elements in the buffer
void printBuffer()
{
uint8_t size = getSize();
if (size > 0)
{
uint8_t index = front;
uint8_t count = 0;
cout << "buffer ";
while (count < bufferSize)
{
cout << " " << bufferData[index];
index = (index+1) % maxSize;
count++;
}
cout << endl;
}
else
{
cout << "empty buffer " << endl;
}
}