Circular / Cyclic Buffer implementation
up vote
2
down vote
favorite
I am learning circular / cyclic buffer using this link. Here is the code that I have written so far:
#include <iostream>
#include <memory>
#include <cstddef>
class CircularBuffer
{
std::unique_ptr<int> u_ptr;
std::size_t head;
std::size_t tail;
const std::size_t capacity;
bool full_v;
public:
CircularBuffer(std::size_t);
void reset();
bool full() const;
bool empty() const;
std::size_t size() const;
void write(int);
int read();
};
// Constructor
// Set the capacity via initializer list because it is const
CircularBuffer::CircularBuffer(std::size_t space):capacity{space}
{
u_ptr = std::unique_ptr<int>(new int[space]);
head = 0;
tail = 0;
full_v = false;
}
// Reset the Buffer
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
u_ptr[head] = int{};
}
// Check if buffer is full
bool CircularBuffer::full() const
{
return full_v;
}
// Check if buffer is empty
bool CircularBuffer::empty() const
{
return (!full_v && head == tail);
}
// Return the size of the buffer
std::size_t CircularBuffer::size() const
{
if(full_v)
{
return capacity;
}
if(head >= tail)
{
return head - tail;
}
else
{
return capacity - (tail - head);
}
}
// Write values into the buffer
void CircularBuffer::write(int data)
{
u_ptr[head] = data;
head = (head + 1) % capacity;
if(full_v)
{
tail = (tail + 1) % capacity;
}
full_v = (head == tail);
}
// Read from buffer
int CircularBuffer::read()
{
if(this -> empty())
{
return int{};
}
int ret_val = u_ptr[tail];
full_v = false;
tail = (tail + 1) % capacity;
return ret_val;
}
int main()
{
CircularBuffer cb{10};
std::cout << "Empty: " << cb.empty() << "n";
for(int i = 0; i < 10; i++)
{
cb.write(i);
}
std::cout << "Full: " << cb.full() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Full: " << cb.full() << "n";
std::cout << "Empty: " << cb.empty() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.write(35);
std::cout << "Size: " << cb.size() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.reset();
std::cout << "Size: " << cb.size() << "n";
return 0;
}
Is the implementation correct? What are the shortcomings that can be fixed?
I see that the original reference uses a
mutex
object and uses lock. Is such an approach used with all data structures? If not, is there a reason why that has been used here?
c++ pointers locking circular-list
add a comment |
up vote
2
down vote
favorite
I am learning circular / cyclic buffer using this link. Here is the code that I have written so far:
#include <iostream>
#include <memory>
#include <cstddef>
class CircularBuffer
{
std::unique_ptr<int> u_ptr;
std::size_t head;
std::size_t tail;
const std::size_t capacity;
bool full_v;
public:
CircularBuffer(std::size_t);
void reset();
bool full() const;
bool empty() const;
std::size_t size() const;
void write(int);
int read();
};
// Constructor
// Set the capacity via initializer list because it is const
CircularBuffer::CircularBuffer(std::size_t space):capacity{space}
{
u_ptr = std::unique_ptr<int>(new int[space]);
head = 0;
tail = 0;
full_v = false;
}
// Reset the Buffer
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
u_ptr[head] = int{};
}
// Check if buffer is full
bool CircularBuffer::full() const
{
return full_v;
}
// Check if buffer is empty
bool CircularBuffer::empty() const
{
return (!full_v && head == tail);
}
// Return the size of the buffer
std::size_t CircularBuffer::size() const
{
if(full_v)
{
return capacity;
}
if(head >= tail)
{
return head - tail;
}
else
{
return capacity - (tail - head);
}
}
// Write values into the buffer
void CircularBuffer::write(int data)
{
u_ptr[head] = data;
head = (head + 1) % capacity;
if(full_v)
{
tail = (tail + 1) % capacity;
}
full_v = (head == tail);
}
// Read from buffer
int CircularBuffer::read()
{
if(this -> empty())
{
return int{};
}
int ret_val = u_ptr[tail];
full_v = false;
tail = (tail + 1) % capacity;
return ret_val;
}
int main()
{
CircularBuffer cb{10};
std::cout << "Empty: " << cb.empty() << "n";
for(int i = 0; i < 10; i++)
{
cb.write(i);
}
std::cout << "Full: " << cb.full() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Full: " << cb.full() << "n";
std::cout << "Empty: " << cb.empty() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.write(35);
std::cout << "Size: " << cb.size() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.reset();
std::cout << "Size: " << cb.size() << "n";
return 0;
}
Is the implementation correct? What are the shortcomings that can be fixed?
I see that the original reference uses a
mutex
object and uses lock. Is such an approach used with all data structures? If not, is there a reason why that has been used here?
c++ pointers locking circular-list
add a comment |
up vote
2
down vote
favorite
up vote
2
down vote
favorite
I am learning circular / cyclic buffer using this link. Here is the code that I have written so far:
#include <iostream>
#include <memory>
#include <cstddef>
class CircularBuffer
{
std::unique_ptr<int> u_ptr;
std::size_t head;
std::size_t tail;
const std::size_t capacity;
bool full_v;
public:
CircularBuffer(std::size_t);
void reset();
bool full() const;
bool empty() const;
std::size_t size() const;
void write(int);
int read();
};
// Constructor
// Set the capacity via initializer list because it is const
CircularBuffer::CircularBuffer(std::size_t space):capacity{space}
{
u_ptr = std::unique_ptr<int>(new int[space]);
head = 0;
tail = 0;
full_v = false;
}
// Reset the Buffer
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
u_ptr[head] = int{};
}
// Check if buffer is full
bool CircularBuffer::full() const
{
return full_v;
}
// Check if buffer is empty
bool CircularBuffer::empty() const
{
return (!full_v && head == tail);
}
// Return the size of the buffer
std::size_t CircularBuffer::size() const
{
if(full_v)
{
return capacity;
}
if(head >= tail)
{
return head - tail;
}
else
{
return capacity - (tail - head);
}
}
// Write values into the buffer
void CircularBuffer::write(int data)
{
u_ptr[head] = data;
head = (head + 1) % capacity;
if(full_v)
{
tail = (tail + 1) % capacity;
}
full_v = (head == tail);
}
// Read from buffer
int CircularBuffer::read()
{
if(this -> empty())
{
return int{};
}
int ret_val = u_ptr[tail];
full_v = false;
tail = (tail + 1) % capacity;
return ret_val;
}
int main()
{
CircularBuffer cb{10};
std::cout << "Empty: " << cb.empty() << "n";
for(int i = 0; i < 10; i++)
{
cb.write(i);
}
std::cout << "Full: " << cb.full() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Full: " << cb.full() << "n";
std::cout << "Empty: " << cb.empty() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.write(35);
std::cout << "Size: " << cb.size() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.reset();
std::cout << "Size: " << cb.size() << "n";
return 0;
}
Is the implementation correct? What are the shortcomings that can be fixed?
I see that the original reference uses a
mutex
object and uses lock. Is such an approach used with all data structures? If not, is there a reason why that has been used here?
c++ pointers locking circular-list
I am learning circular / cyclic buffer using this link. Here is the code that I have written so far:
#include <iostream>
#include <memory>
#include <cstddef>
class CircularBuffer
{
std::unique_ptr<int> u_ptr;
std::size_t head;
std::size_t tail;
const std::size_t capacity;
bool full_v;
public:
CircularBuffer(std::size_t);
void reset();
bool full() const;
bool empty() const;
std::size_t size() const;
void write(int);
int read();
};
// Constructor
// Set the capacity via initializer list because it is const
CircularBuffer::CircularBuffer(std::size_t space):capacity{space}
{
u_ptr = std::unique_ptr<int>(new int[space]);
head = 0;
tail = 0;
full_v = false;
}
// Reset the Buffer
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
u_ptr[head] = int{};
}
// Check if buffer is full
bool CircularBuffer::full() const
{
return full_v;
}
// Check if buffer is empty
bool CircularBuffer::empty() const
{
return (!full_v && head == tail);
}
// Return the size of the buffer
std::size_t CircularBuffer::size() const
{
if(full_v)
{
return capacity;
}
if(head >= tail)
{
return head - tail;
}
else
{
return capacity - (tail - head);
}
}
// Write values into the buffer
void CircularBuffer::write(int data)
{
u_ptr[head] = data;
head = (head + 1) % capacity;
if(full_v)
{
tail = (tail + 1) % capacity;
}
full_v = (head == tail);
}
// Read from buffer
int CircularBuffer::read()
{
if(this -> empty())
{
return int{};
}
int ret_val = u_ptr[tail];
full_v = false;
tail = (tail + 1) % capacity;
return ret_val;
}
int main()
{
CircularBuffer cb{10};
std::cout << "Empty: " << cb.empty() << "n";
for(int i = 0; i < 10; i++)
{
cb.write(i);
}
std::cout << "Full: " << cb.full() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Full: " << cb.full() << "n";
std::cout << "Empty: " << cb.empty() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.write(35);
std::cout << "Size: " << cb.size() << "n";
std::cout << "Read: " << cb.read() << "n";
std::cout << "Size: " << cb.size() << "n";
cb.reset();
std::cout << "Size: " << cb.size() << "n";
return 0;
}
Is the implementation correct? What are the shortcomings that can be fixed?
I see that the original reference uses a
mutex
object and uses lock. Is such an approach used with all data structures? If not, is there a reason why that has been used here?
c++ pointers locking circular-list
c++ pointers locking circular-list
edited Sep 30 at 21:08
Jamal♦
30.2k11115226
30.2k11115226
asked Sep 30 at 18:27
skr_robo
304210
304210
add a comment |
add a comment |
2 Answers
2
active
oldest
votes
up vote
2
down vote
accepted
Your ringbuffer has a compile time size, so it would be appropriate to make it a template class rather than simply passing it as an argument to the constructor. You never plan to change it anyway?
template<size_t bufSize>
class CircularBuffer
That leads to the next point. You use a std::unique_ptr<int>
There is something in C++ that is much better than this std::array
. With the size of the buffer a compile time template argument of the class you can easily use it
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head;
std::size_t tail;
bool full_v;
...
};
Note that this increases the size of the class object considerably, as the std::array
is directly inlined in the class.
The other members of this class are always the same after construction so you should use static member initialization:
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head{ 0 };
std::size_t tail{ 0 };
bool full_v{ false };
...
};
Note that this completely removes the need to define a constructor at all. The compiler generated default constructor will do just fine.
Resetting the buffer should not change the data stored in it as it is not read anyway so just leave it alone
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
}
Can tail ever be larger than head? If not why are you checking it.
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
add a comment |
up vote
2
down vote
Correctness and design
I will skip all points miscco
already raised and go directly to void write
and int read
.
- Your
write
overwrites old values when the buffer is full. - Your
read
returns zero if the buffer is empty.
Although understandable, it should at least be documented:
/// Describe the class, what is the purpose, what is important
template<class T, size_t N> // see std::array
class CircularBuffer {
...
/// Write values into the buffer, overwrite old values when the buffer is full
void write(const T& data) {
or maybe:
/// Write values into the buffer
///return true if written, false if buffer was full
bool write(const T& data) {
You can also notice that I made it a template, just like std::array
- both the type and the capacity are now template parameters. The standard way of passing arguments to methods like write
is to use const T&
, but it has some drawbacks, so, if it trully is for embedded systems, then simple T
is an option as well (knowing you are going to use it with integers and such). But document it (and/or add other template parameters with some defaults).
Synchronization (Locking)
If you are going to use it with multiple threads (or main loop vs. interrupt) then some form of synchronization is indeed needed, because there are possible race-conditions otherwise, like one thread being inside write
, just overwriting the oldest slot, while another thread can be inside read
getting this newest element, while it should get the oldest, but tail
was not yet updated by the first thread.
First question to ask is: How many threads / concurrent readers and writers (producers and consumers)?
Generic solution could use mutex
but there can be better solutions for single producer single consumer (e.g. main loop vs. interrupt - USART and similar communication synchronization). One possibility is to use separate (atomic) write and read counters (like head and tail but you always only increase the counters and substract them to get number of elements inside the buffer - but be careful with order of operations, it is not trivial).
add a comment |
Your Answer
StackExchange.ifUsing("editor", function () {
return StackExchange.using("mathjaxEditing", function () {
StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
});
});
}, "mathjax-editing");
StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "196"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f204644%2fcircular-cyclic-buffer-implementation%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
2
down vote
accepted
Your ringbuffer has a compile time size, so it would be appropriate to make it a template class rather than simply passing it as an argument to the constructor. You never plan to change it anyway?
template<size_t bufSize>
class CircularBuffer
That leads to the next point. You use a std::unique_ptr<int>
There is something in C++ that is much better than this std::array
. With the size of the buffer a compile time template argument of the class you can easily use it
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head;
std::size_t tail;
bool full_v;
...
};
Note that this increases the size of the class object considerably, as the std::array
is directly inlined in the class.
The other members of this class are always the same after construction so you should use static member initialization:
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head{ 0 };
std::size_t tail{ 0 };
bool full_v{ false };
...
};
Note that this completely removes the need to define a constructor at all. The compiler generated default constructor will do just fine.
Resetting the buffer should not change the data stored in it as it is not read anyway so just leave it alone
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
}
Can tail ever be larger than head? If not why are you checking it.
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
add a comment |
up vote
2
down vote
accepted
Your ringbuffer has a compile time size, so it would be appropriate to make it a template class rather than simply passing it as an argument to the constructor. You never plan to change it anyway?
template<size_t bufSize>
class CircularBuffer
That leads to the next point. You use a std::unique_ptr<int>
There is something in C++ that is much better than this std::array
. With the size of the buffer a compile time template argument of the class you can easily use it
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head;
std::size_t tail;
bool full_v;
...
};
Note that this increases the size of the class object considerably, as the std::array
is directly inlined in the class.
The other members of this class are always the same after construction so you should use static member initialization:
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head{ 0 };
std::size_t tail{ 0 };
bool full_v{ false };
...
};
Note that this completely removes the need to define a constructor at all. The compiler generated default constructor will do just fine.
Resetting the buffer should not change the data stored in it as it is not read anyway so just leave it alone
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
}
Can tail ever be larger than head? If not why are you checking it.
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
add a comment |
up vote
2
down vote
accepted
up vote
2
down vote
accepted
Your ringbuffer has a compile time size, so it would be appropriate to make it a template class rather than simply passing it as an argument to the constructor. You never plan to change it anyway?
template<size_t bufSize>
class CircularBuffer
That leads to the next point. You use a std::unique_ptr<int>
There is something in C++ that is much better than this std::array
. With the size of the buffer a compile time template argument of the class you can easily use it
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head;
std::size_t tail;
bool full_v;
...
};
Note that this increases the size of the class object considerably, as the std::array
is directly inlined in the class.
The other members of this class are always the same after construction so you should use static member initialization:
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head{ 0 };
std::size_t tail{ 0 };
bool full_v{ false };
...
};
Note that this completely removes the need to define a constructor at all. The compiler generated default constructor will do just fine.
Resetting the buffer should not change the data stored in it as it is not read anyway so just leave it alone
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
}
Can tail ever be larger than head? If not why are you checking it.
Your ringbuffer has a compile time size, so it would be appropriate to make it a template class rather than simply passing it as an argument to the constructor. You never plan to change it anyway?
template<size_t bufSize>
class CircularBuffer
That leads to the next point. You use a std::unique_ptr<int>
There is something in C++ that is much better than this std::array
. With the size of the buffer a compile time template argument of the class you can easily use it
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head;
std::size_t tail;
bool full_v;
...
};
Note that this increases the size of the class object considerably, as the std::array
is directly inlined in the class.
The other members of this class are always the same after construction so you should use static member initialization:
template<size_t bufSize>
class CircularBuffer {
std::array<int, bufSize> buf;
std::size_t head{ 0 };
std::size_t tail{ 0 };
bool full_v{ false };
...
};
Note that this completely removes the need to define a constructor at all. The compiler generated default constructor will do just fine.
Resetting the buffer should not change the data stored in it as it is not read anyway so just leave it alone
void CircularBuffer::reset()
{
head = 0;
tail = 0;
full_v = false;
}
Can tail ever be larger than head? If not why are you checking it.
answered Sep 30 at 18:55
miscco
3,387517
3,387517
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
add a comment |
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
Thank You for the review. Imagine a situation where buffer is full and both head and tail are in the middle of the buffer. Then reading happens. The tail keeps increasing while head remains in its position. So, in this case, tail can be ahead of head.
– skr_robo
Sep 30 at 19:03
add a comment |
up vote
2
down vote
Correctness and design
I will skip all points miscco
already raised and go directly to void write
and int read
.
- Your
write
overwrites old values when the buffer is full. - Your
read
returns zero if the buffer is empty.
Although understandable, it should at least be documented:
/// Describe the class, what is the purpose, what is important
template<class T, size_t N> // see std::array
class CircularBuffer {
...
/// Write values into the buffer, overwrite old values when the buffer is full
void write(const T& data) {
or maybe:
/// Write values into the buffer
///return true if written, false if buffer was full
bool write(const T& data) {
You can also notice that I made it a template, just like std::array
- both the type and the capacity are now template parameters. The standard way of passing arguments to methods like write
is to use const T&
, but it has some drawbacks, so, if it trully is for embedded systems, then simple T
is an option as well (knowing you are going to use it with integers and such). But document it (and/or add other template parameters with some defaults).
Synchronization (Locking)
If you are going to use it with multiple threads (or main loop vs. interrupt) then some form of synchronization is indeed needed, because there are possible race-conditions otherwise, like one thread being inside write
, just overwriting the oldest slot, while another thread can be inside read
getting this newest element, while it should get the oldest, but tail
was not yet updated by the first thread.
First question to ask is: How many threads / concurrent readers and writers (producers and consumers)?
Generic solution could use mutex
but there can be better solutions for single producer single consumer (e.g. main loop vs. interrupt - USART and similar communication synchronization). One possibility is to use separate (atomic) write and read counters (like head and tail but you always only increase the counters and substract them to get number of elements inside the buffer - but be careful with order of operations, it is not trivial).
add a comment |
up vote
2
down vote
Correctness and design
I will skip all points miscco
already raised and go directly to void write
and int read
.
- Your
write
overwrites old values when the buffer is full. - Your
read
returns zero if the buffer is empty.
Although understandable, it should at least be documented:
/// Describe the class, what is the purpose, what is important
template<class T, size_t N> // see std::array
class CircularBuffer {
...
/// Write values into the buffer, overwrite old values when the buffer is full
void write(const T& data) {
or maybe:
/// Write values into the buffer
///return true if written, false if buffer was full
bool write(const T& data) {
You can also notice that I made it a template, just like std::array
- both the type and the capacity are now template parameters. The standard way of passing arguments to methods like write
is to use const T&
, but it has some drawbacks, so, if it trully is for embedded systems, then simple T
is an option as well (knowing you are going to use it with integers and such). But document it (and/or add other template parameters with some defaults).
Synchronization (Locking)
If you are going to use it with multiple threads (or main loop vs. interrupt) then some form of synchronization is indeed needed, because there are possible race-conditions otherwise, like one thread being inside write
, just overwriting the oldest slot, while another thread can be inside read
getting this newest element, while it should get the oldest, but tail
was not yet updated by the first thread.
First question to ask is: How many threads / concurrent readers and writers (producers and consumers)?
Generic solution could use mutex
but there can be better solutions for single producer single consumer (e.g. main loop vs. interrupt - USART and similar communication synchronization). One possibility is to use separate (atomic) write and read counters (like head and tail but you always only increase the counters and substract them to get number of elements inside the buffer - but be careful with order of operations, it is not trivial).
add a comment |
up vote
2
down vote
up vote
2
down vote
Correctness and design
I will skip all points miscco
already raised and go directly to void write
and int read
.
- Your
write
overwrites old values when the buffer is full. - Your
read
returns zero if the buffer is empty.
Although understandable, it should at least be documented:
/// Describe the class, what is the purpose, what is important
template<class T, size_t N> // see std::array
class CircularBuffer {
...
/// Write values into the buffer, overwrite old values when the buffer is full
void write(const T& data) {
or maybe:
/// Write values into the buffer
///return true if written, false if buffer was full
bool write(const T& data) {
You can also notice that I made it a template, just like std::array
- both the type and the capacity are now template parameters. The standard way of passing arguments to methods like write
is to use const T&
, but it has some drawbacks, so, if it trully is for embedded systems, then simple T
is an option as well (knowing you are going to use it with integers and such). But document it (and/or add other template parameters with some defaults).
Synchronization (Locking)
If you are going to use it with multiple threads (or main loop vs. interrupt) then some form of synchronization is indeed needed, because there are possible race-conditions otherwise, like one thread being inside write
, just overwriting the oldest slot, while another thread can be inside read
getting this newest element, while it should get the oldest, but tail
was not yet updated by the first thread.
First question to ask is: How many threads / concurrent readers and writers (producers and consumers)?
Generic solution could use mutex
but there can be better solutions for single producer single consumer (e.g. main loop vs. interrupt - USART and similar communication synchronization). One possibility is to use separate (atomic) write and read counters (like head and tail but you always only increase the counters and substract them to get number of elements inside the buffer - but be careful with order of operations, it is not trivial).
Correctness and design
I will skip all points miscco
already raised and go directly to void write
and int read
.
- Your
write
overwrites old values when the buffer is full. - Your
read
returns zero if the buffer is empty.
Although understandable, it should at least be documented:
/// Describe the class, what is the purpose, what is important
template<class T, size_t N> // see std::array
class CircularBuffer {
...
/// Write values into the buffer, overwrite old values when the buffer is full
void write(const T& data) {
or maybe:
/// Write values into the buffer
///return true if written, false if buffer was full
bool write(const T& data) {
You can also notice that I made it a template, just like std::array
- both the type and the capacity are now template parameters. The standard way of passing arguments to methods like write
is to use const T&
, but it has some drawbacks, so, if it trully is for embedded systems, then simple T
is an option as well (knowing you are going to use it with integers and such). But document it (and/or add other template parameters with some defaults).
Synchronization (Locking)
If you are going to use it with multiple threads (or main loop vs. interrupt) then some form of synchronization is indeed needed, because there are possible race-conditions otherwise, like one thread being inside write
, just overwriting the oldest slot, while another thread can be inside read
getting this newest element, while it should get the oldest, but tail
was not yet updated by the first thread.
First question to ask is: How many threads / concurrent readers and writers (producers and consumers)?
Generic solution could use mutex
but there can be better solutions for single producer single consumer (e.g. main loop vs. interrupt - USART and similar communication synchronization). One possibility is to use separate (atomic) write and read counters (like head and tail but you always only increase the counters and substract them to get number of elements inside the buffer - but be careful with order of operations, it is not trivial).
edited 1 hour ago
albert
1371
1371
answered Oct 1 at 12:41
firda
2,842627
2,842627
add a comment |
add a comment |
Thanks for contributing an answer to Code Review Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
Use MathJax to format equations. MathJax reference.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f204644%2fcircular-cyclic-buffer-implementation%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown