Best way of implementing a circular buffer

I wanted to implement a circular buffer for learning purpose only.

My first option was to use a secondary status for rear and front pointers: (Like the ones I've seen in many websites)

#include <iostream>
using namespace std;

template<class T>
class ql
{
    public:
        ql(int size)
        {
            this->size = size;
            data = new T[size];
            front = NULL;
            rear = NULL;
        }

        ~ql()
        {
            delete[] data;
        }

        void enQueue(T item)
        {
            T *nf = nextPtr(rear);

            if (nf != front)
            {
                if (front == NULL)
                    front = &data[0];

                *nf = item;
                rear = nf;
                cout << item << " Added. ^_^" << endl;
            }
            else
                cout << "OverFLO@#R$MR... X_X" << endl;
        }

        T *deQueue()
        {
            if (rear != NULL)
            {
                T *p = front;
                if (front == rear)
                {
                    front = NULL;
                    rear = NULL;
                } 
                else
                    front = nextPtr(front);
                cout << *p << " is going to be returned. -_-" << endl;
                return p;
            }
            else
                cout << "Empty... >_<" << endl;
        }
    private:
        T *nextPtr(T *p)
        {
            if (p == &data[size - 1] || p == NULL)
                return &data[0];
            return p + 1;
        }

        T *data, *rear, *front;
        int size;
};

int main()
{
    ql<int> q(3);

    q.enQueue(1);
    q.enQueue(2);
    q.enQueue(3);
    q.enQueue(4);

    cout << endl;
    q.deQueue();
    q.deQueue();
    q.deQueue();
    q.deQueue();

    cout << endl;
    q.enQueue(5);
    q.enQueue(6);

    cout << endl;
    q.deQueue();
    q.deQueue();
    q.deQueue();

    return 0;
}

My second option was to sacrifice a space for the sake of distinguishing between empty and full circular buffers: (I saw this one on Ellis's Fundamentals of data structures)

template<class T>
class ql
{
    public:
        ql(int size)
        {
            this->size = size;
            data = new T[size];
            front = 1;
            rear = 0;
        }

        ~ql()
        {
            delete[] data;
        }

        void enQueue(T item)
        {
            if ((rear + 2) % size != front)
            {
                rear = (rear + 1) % size;
                data[rear] = item;
                cout << item << " Added. ^_^" << endl;
            }
            else
                cout << "OverFLO@#R$MR... X_X" << endl;
        }

        T *deQueue()
        {
            if ((rear + 1) % size != front)
            {
                T *p = &data[front];
                cout << *p << " is going to be returned. -_-" << endl;
                front = (front + 1) % size;
                return p;
            }
            else
                cout << "Empty... >_<" << endl;
        }
    private:
        T *data;
        int size, rear, front;
};

and my last option was to use another variable for storing used space in circular buffer:

template<class T>
class ql
{
    public:
        ql(int size)
        {
            this->size = size;
            data = new T[size];
            buffer = 0;
            front = 1;
            rear = 0;
        }

        ~ql()
        {
            delete[] data;
        }

        void enQueue(T item)
        {
            if (buffer != size)
            {
                buffer++;
                rear = (rear + 1) % size;
                data[rear] = item;
                cout << item << " Added. ^_^" << endl;
            }
            else
                cout << "OverFLO@#R$MR... X_X" << endl;
        }

        T *deQueue()
        {
            if (buffer != 0)
            {
                buffer--;
                T *p = &data[front];
                cout << *p << " is going to be returned. -_-" << endl;
                front = (front + 1) % size;
                return p;
            }
            else
                cout << "Empty... >_<" << endl;
        }
    private:
        T *data;
        int size, buffer, rear, front;
};

Which one of this approaches do you think is the best? I'm also looking for advises on how to change this class for practical using. thanks

 

Use better names and do not use using namespace in headers

The name q1 is rather arbitrary. queue or circular_queue is a lot better. That, by the way, is a perfect example why you shouldn't use using namespace std; when you write a header. There's already std::queue, so a queue would conflict with std::queue.

Since you're writing a template class all your code will reside in a header at some point, so using namespace is out of the question either way.

Use a smarter data store

Use std::vector<T> or std::deque<T> instead of raw pointers for the memory. Or re-use std::queue<T>, unless you want to practice writing a queue completely by hand.

Use return instead of cout

Instead of cout << … << return bool or a custom enum in enQueue. If I want to store elements in a circular buffer, I need to know whether enQueue worked. I cannot check stdout for error messages.

Sizes are positive

Use size_t for sizes, not int.

Check all return paths

Return nullptr (C++11 or higher) or 0 in deQueue if the queue is empty. However, a pointer at that point is dangerous: the user must make a copy at some point, or they might end up with another object. Use std::optional if you have C++17 at hand instead, or

 bool deQueue(T & dest) {
     if(…) {
         // queue has elements
         dest = …;
         …
         return true;
     } else {
         // queue has no elements
         return false;
     }
 }

enQueue could use a const T& instead of a T, by the way. Or you could use std::move for movable types.

All at once

If you follow these guidelines, you will end up

#include <optional>
#include <queue>
#include <utility>

template<class T>
class queue
{
    public:
        explicit queue(size_t size) : m_size(size) { }

        queue(const queue<T> & other) = default;
        queue(queue<T> && other) = default;
        queue& operator=(const queue<T> & other) = default;
        queue& operator=(queue<T> && other) = default;

        bool enQueue(T item)
        {
            if(m_data.size() == m_size) {
                return false;
            } else {
                m_data.push(std::move(item));
                return true;
            }
        }

        std::optional<T> deQueue()
        {
            if(m_data.empty()) {
                return std::nullopt;
            } else {
                std::optional<T> result = m_data.front();
                m_data.pop();
                return result;
            }
        }

        size_t capacity() const
        {
            return m_size;
        }

        size_t size() const
        {
            return m_data.size();
        }
    private:
        size_t m_size;
        std::queue<T> m_data;
};

If you don't want to re-use std::queue or std::deque, I'd go with std::vector and your third approach.

Congratulations on your first approach, by the way. I've seen raw-pointer usage going wrong too many times, and it's refreshing to see some clever use there. Well done. But that's more or less the way you would do it in C (sans template and class, of course).

But you will probably admit that working with pointers at that point is somewhat a headache. Both front and rear (as pointers) can be expressed as data + x and data + y for two suitable int x and y, like you did in your second approach.

Either way, unless you really need to use raw pointers use either smart pointers (e.g. std::unique_ptr<T[]>) or (better) full containers like std::vector.

 

copyright

https://codereview.stackexchange.com/questions/180556/best-way-of-implementing-a-circular-buffer

posted @ 2021-01-28 13:47  dong1  阅读(70)  评论(0编辑  收藏  举报