Reputation: 429
Recently I had a job interview and I was asked to implement circular buffer class. It was required to not use any containers (incl. STL).
Mine code was following:
template<class T>
class CircularFifo
{
T * _data;
size_t _size;
size_t _read; // last readen elem
size_t _write; // next write index
CircularFifo(CircularFifo const &) = delete;
CircularFifo & operator=(CircularFifo const &) = delete;
CircularFifo(CircularFifo &&) = delete;
CircularFifo & operator=(CircularFifo &&) = delete;
public:
explicit inline
CircularFifo(size_t size = 2048)
: _data(new T[size])
, _size(size)
, _read(-1)
, _write(0)
{
if (0 == _size)
{
throw std::runtime_error("too empty buffer");
}
if (1 == _size)
{
throw std::runtime_error("too short buffer");
}
if (-1 == size)
{
throw std::runtime_error("too huge buffer");
}
}
inline ~CircularFifo()
{
delete []_data;
}
inline T read()
{
if (_read == _write)
{
throw std::runtime_error("buffer underflow");
}
return _data[(++_read) % _size];
}
inline void write(T const & obj)
{
if (_read == _write)
{
throw std::runtime_error("buffer overflow");
}
_data[(_write++) % _size] = obj;
}
};
An interviewer sad that coding style is fine. But there is a bug in the buffer which will makes this class unreliable. She asked me to find it and I totally failed. She also haven't disclosed this bug to me too.
I re-checked everything: leaks, arithmetics, possible overflows etc. My head almost ready to explode. I don't know where is mistake. Please help me.
P.S. Sorry for my messy English.
Upvotes: 12
Views: 1865
Reputation: 746
In addition to mistake with initialization you have also this problem:
when you add element, you check (_read == _write), then do _write++. So you can write without reading until you _write field will overflow its type. You need to check ( (_write -_read) == _size) before writing, not (_read == _write)
Upvotes: 0
Reputation: 96
There are several bugs in your code.
First, as already pointed out in another answer, by initializing _read
to -1, if you try to read before writing, you will read garbage.
Second, if you don't read anything, but you write size
items, _write
will wrap around and overwrite the start of the buffer, as _read
will never equal _write
.
But even if you did initialize both _read
and _write
to valid values, it wouldn't work because in your code, the condition _read == _write
means both "buffer is empty" and "buffer is full". One of the two indices must stop one element before the other. If _read == _write
means "buffer empty", then (_read+1)%size == _write
should mean "buffer full".
By implementing the circular buffer this way, you will always waste one element (you can store a maximum of size-1
items).
There is another way of implementing a circular buffer that doesn't waste an element. Here's an article explaining both ways of implementing a circular buffer.
Upvotes: 8
Reputation: 1219
When _read
or _write
overflow you have a problem. When the size is not a power of 2 you skip elements. This is not be an expected behaviour in circular buffer.
They should be assigned modulo _size
after each operation.
Also, as a side note, I would not make size
a default parameter with an arbitrary value. If the user doesn't know what size they should use they don't understand the purpose and dangers of the data structure.
Second note. I think the noncritical exceptions, like "buffer over/underflow" should be debug only asserts (also maybe zero size one). If something like this happens it is a huge bug made by a user, not an exceptional case. They arise from logical mistake in user's code, not from uncontrollable limitations like for example amount of addressable memory.
Size of one should be perfectly fine too though.
Upvotes: 0
Reputation: 366
Could it be that _read
is initialized to -1 and _write
is initialized to 0, but in the read function you check if they are equal? When the buffer is first initialized, and a read is attempted, the if check will succeed even though nothing has been added to the buffer.
Upvotes: 16