I tried implementing a thread-safe ring queue in C++. I'm totally new to move semantics and C++11/14/17 in general.
#ifndef THREAD_SAFE_RING_QUEUE_HPP_
#define THREAD_SAFE_RING_QUEUE_HPP_
#include <mutex>
#include <optional>
#include <vector>
namespace structure {
class MaximumCapacityReachedError : public std::runtime_error {
public:
MaximumCapacityReachedError(std::size_t capacity)
: runtime_error("Structure instance is full (" +
std::to_string(capacity) + ")") {}
};
class EmptyError : public std::runtime_error {
public:
EmptyError() : runtime_error("Structure instance is empty") {}
};
template <class T>
class RingQueue {
public:
static constexpr std::size_t kDefaultCapacity_ = 10;
RingQueue(std::size_t = kDefaultCapacity_);
bool IsEmpty() const;
std::size_t GetSize() const;
void Push(T&& element);
void TryPush(T&& element);
T& ConsumeNext();
std::optional<T> TryConsumeNext();
void Clear();
std::size_t GetCapacity() const noexcept;
bool IsFull() const noexcept;
private:
mutable std::recursive_mutex mutex_;
std::size_t capacity_ = 0;
std::size_t head_ = 0;
std::size_t tail_ = 0;
std::vector<T> elements_;
bool is_full_ = false;
};
template <class T>
inline RingQueue<T>::RingQueue(std::size_t capacity)
: capacity_(capacity),
elements_(std::vector<T>(capacity_)),
is_full_(capacity == 0) {}
template <class T>
inline bool RingQueue<T>::IsEmpty() const {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
return capacity_ == 0 || (!is_full_ && head_ == tail_);
}
template <class T>
inline std::size_t RingQueue<T>::GetSize() const {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
if (is_full_) {
return capacity_;
}
if (tail_ >= head_) {
return tail_ - head_;
}
return capacity_ - (head_ - tail_);
}
template <class T>
inline void RingQueue<T>::Push(T&& element) {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
if (IsFull()) {
throw MaximumCapacityReachedError(capacity_);
}
elements_[tail_] = std::forward<T>(element);
tail_ = (tail_ + 1) % capacity_;
is_full_ = head_ == tail_;
}
template <class T>
inline void RingQueue<T>::TryPush(T&& element) {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
if (IsFull()) {
return;
}
elements_[tail_] = std::forward<T>(element);
tail_ = (tail_ + 1) % capacity_;
is_full_ = head_ == tail_;
}
template <class T>
inline T& RingQueue<T>::ConsumeNext() {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
if (IsEmpty()) {
throw EmptyError();
}
const auto previous_head = head_;
head_ = (head_ + 1) % capacity_;
is_full_ = false;
return std::forward<T>(elements_[previous_head]);
}
template <class T>
inline std::optional<T> RingQueue<T>::TryConsumeNext() {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
if (IsEmpty()) {
return std::nullopt;
}
const auto previous_head = head_;
head_ = (head_ + 1) % capacity_;
is_full_ = false;
return std::forward<T>(elements_[previous_head]);
}
template <class T>
inline void RingQueue<T>::Clear() {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
head_ = 0;
tail_ = 0;
is_full_ = false;
}
template <class T>
inline std::size_t RingQueue<T>::GetCapacity() const noexcept {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
return capacity_;
}
template <class T>
inline bool RingQueue<T>::IsFull() const noexcept {
std::scoped_lock<std::recursive_mutex> lock(mutex_);
return is_full_;
}
} // namespace structure
#endif // THREAD_SAFE_RING_QUEUE_HPP_
I have tested my code and it seems to work, but so many things can go wrong...
EDIT: here are the tests that I've done using Catch2. DummyObject
is just a class containing an int
(the value may be passed in the constructor, and is 0
by default).
#include <memory>
#include "catch.hpp"
#include "dummies.hpp"
#include "utils/structure/ring_queue.hpp"
TEST_CASE("Ring queue creation with default capacity", "[structure]") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>();
SECTION("Properties after creation with default capacity.") {
REQUIRE(queue.GetCapacity() == queue.kDefaultCapacity_);
REQUIRE(queue.GetSize() == 0);
REQUIRE(!queue.IsFull());
REQUIRE(queue.IsEmpty());
}
}
TEST_CASE("Ring queue creation with specific capacity", "[structure]") {
const std::size_t capacity = 15;
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(capacity);
SECTION("Properties after creation with specific capacity.") {
REQUIRE(queue.GetCapacity() == capacity);
REQUIRE(queue.GetSize() == 0);
REQUIRE(!queue.IsFull());
REQUIRE(queue.IsEmpty());
}
}
TEST_CASE("Ring queue push operations in single thread", "[structure]") {
const std::size_t capacity = 15;
SECTION("Properties after pushing one element.") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
const auto dummy1 = dummy::DummyObject(0);
queue.Push(std::make_unique<dummy::DummyObject>(0));
REQUIRE(queue.GetSize() == 1);
REQUIRE(!queue.IsFull());
REQUIRE(!queue.IsEmpty());
}
SECTION("Properties after pushing two elements.") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
queue.Push(std::make_unique<dummy::DummyObject>(0));
queue.Push(std::make_unique<dummy::DummyObject>(0));
REQUIRE(queue.GetSize() == 2);
REQUIRE(!queue.IsFull());
REQUIRE(!queue.IsEmpty());
}
SECTION("Properties after pushing the maximum amount of elements.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(1);
queue.Push(std::make_unique<dummy::DummyObject>(0));
REQUIRE(queue.GetSize() == 1);
REQUIRE(queue.IsFull());
REQUIRE(!queue.IsEmpty());
}
SECTION("Properties after pushing too many elements.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(1);
queue.Push(std::make_unique<dummy::DummyObject>(0));
bool is_exception = false;
try {
queue.Push(std::make_unique<dummy::DummyObject>(0));
} catch (const structure::MaximumCapacityReachedError& error) {
is_exception = true;
}
REQUIRE(is_exception);
REQUIRE(queue.GetSize() == 1);
REQUIRE(queue.IsFull());
REQUIRE(!queue.IsEmpty());
}
SECTION("Specific error case: pushing one element in a 0-capacity queue.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(0);
bool is_exception = false;
try {
queue.Push(std::make_unique<dummy::DummyObject>(0));
} catch (const structure::MaximumCapacityReachedError& error) {
is_exception = true;
}
REQUIRE(is_exception);
}
SECTION("Try pushing an element.") {
std::size_t capacity = 5;
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
for (std::size_t index = 0; index < capacity + 1; index++) {
queue.TryPush(std::make_unique<dummy::DummyObject>(0));
}
REQUIRE(queue.GetSize() == capacity);
}
}
TEST_CASE("Ring queue consume operations in single thread",
"[structure]") {
const std::size_t capacity = 15;
SECTION("Properties after consuming one element.") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
queue.Push(std::make_unique<dummy::DummyObject>(42));
REQUIRE(queue.ConsumeNext()->GetValue() == 42);
REQUIRE(queue.GetSize() == 0);
REQUIRE(!queue.IsFull());
REQUIRE(queue.IsEmpty());
}
SECTION("Properties after consuming two elements.") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
queue.Push(std::make_unique<dummy::DummyObject>(42));
REQUIRE(queue.ConsumeNext()->GetValue() == 42);
REQUIRE(queue.GetSize() == 0);
REQUIRE(!queue.IsFull());
REQUIRE(queue.IsEmpty());
}
SECTION("Properties after consuming one element in a two-element queue.") {
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
queue.Push(std::make_unique<dummy::DummyObject>(0));
queue.Push(std::make_unique<dummy::DummyObject>(0));
REQUIRE(queue.ConsumeNext());
REQUIRE(queue.GetSize() == 1);
REQUIRE(!queue.IsFull());
REQUIRE(!queue.IsEmpty());
}
SECTION("Properties after consuming too many elements.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(1);
queue.Push(std::make_unique<dummy::DummyObject>(42));
queue.ConsumeNext();
bool is_exception = false;
try {
queue.ConsumeNext();
} catch (const structure::EmptyError& error) {
is_exception = true;
}
REQUIRE(is_exception);
REQUIRE(queue.GetSize() == 0);
REQUIRE(!queue.IsFull());
REQUIRE(queue.IsEmpty());
}
SECTION("Order is respected.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(3);
queue.Push(std::make_unique<dummy::DummyObject>(1));
queue.Push(std::make_unique<dummy::DummyObject>(2));
queue.Push(std::make_unique<dummy::DummyObject>(3));
REQUIRE(queue.ConsumeNext().get()->GetValue() == 1);
REQUIRE(queue.ConsumeNext().get()->GetValue() == 2);
REQUIRE(queue.ConsumeNext().get()->GetValue() == 3);
}
SECTION("Specific error case: consuming one element in a 0-capacity queue.") {
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(0);
bool is_exception = false;
try {
queue.ConsumeNext();
} catch (const structure::EmptyError& error) {
is_exception = true;
}
REQUIRE(is_exception);
}
SECTION("Try consuming an element.") {
std::size_t capacity = 5;
auto queue = structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(
capacity);
for (std::size_t index = 0; index < capacity; index++) {
queue.Push(std::make_unique<dummy::DummyObject>(0));
}
for (std::size_t index = 0; index < capacity + 1; index++) {
const auto element = queue.TryConsumeNext();
const bool is_element = element.has_value();
const bool is_okay = index < capacity ? is_element : !is_element;
REQUIRE(is_okay);
}
REQUIRE(queue.GetSize() == 0);
}
}
TEST_CASE("Ring queue pushing operations in multiple threads",
"[structure]") {
const std::size_t capacity = 15;
SECTION("Usage in threads: push operations.") {
std::size_t thread_number = 5;
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(5);
std::vector<std::thread> threads;
for (std::size_t index = 0; index < thread_number; index++) {
threads.push_back(std::thread([&queue]() {
queue.Push(std::make_unique<dummy::DummyObject>(0));
}));
}
std::for_each(threads.begin(), threads.end(),
[](std::thread& thread) { thread.join(); });
REQUIRE(queue.GetSize() == thread_number);
}
}
TEST_CASE("Ring queue consume operations in multiple threads",
"[structure]") {
SECTION("Usage in threads: consume operations.") {
std::size_t thread_number = 5;
auto queue =
structure::RingQueue<std::shared_ptr<dummy::DummyObject>>(5);
std::vector<std::thread> threads;
for (std::size_t index = 0; index < thread_number; index++) {
queue.Push(std::make_unique<dummy::DummyObject>(0));
}
for (std::size_t index = 0; index < thread_number; index++) {
threads.push_back(std::thread([&queue]() { queue.ConsumeNext(); }));
}
std::for_each(threads.begin(), threads.end(),
[](std::thread& thread) { thread.join(); });
REQUIRE(queue.GetSize() == 0);
}
}
```
tryPush
let me know whether it succeeded? \$\endgroup\$