A Quick Tour#
Using a Queue#
From the consumers’ perspective, the cppdsa-queue
library provides various
ready-to-use queue data structures that can store pretty much any kind of C++
objects of both the fundamental and user-defined types. Each queue
implementation is a class template and has the same standardized API, except
the additional features specific to a particular implementation.
Having known the type T of the elements to store, you instantiate a queue from the chosen class template. It’s often convenient to define a type alias for it upfront. Now, let’s try the unbounded Circular Array Queue.
#include "circ_array_queue.hpp"
using IntQueue = dsa::CircArrayQueue<int>;
auto q = IntQueue {};
Now, you’ve an empty queue q
awaiting data.
for (int nums[] { 3, 1, 4, 1 }; auto num : nums) {
q.enqueue(num);
}
std::cout << q.size() << std::endl; // prints 4
In case of large element type T, you may use the emplace<Args...>()
member function to create the new elements in-place without copying and moving
to boost performance.
In any case, you may then use the typical idiom to process the elements in the queue in the first-in-first-out (FIFO) manner.
while (!q.empty()) {
std::cout << q.front() << ' ';
q.dequeue();
}
std::cout << std::endl;
As long as you’re not using the static polymorphism feature of the library,
you have nothing to worry about q
and its elements when you’re done with it,
unless q
was created on the free store and assigned to a pointer of the
exact same type or simply an auto
pointer. In the latter case, of course
you’ll simply have to do delete q;
.
IntQueue* q = new IntQueue {};
...
delete q; // GOOD: it works
Static Polymorphism#
Now, suppose you’ve created a queue on the free store and assigned it to a pointer to the “abstract” base class template.
IQueue<int, dsa::CircArrayQueue>* q = new dsa::CircArrayQueue<int> {};
...
delete q; // BAD: memory leak!
The last statement is the catch! When you’re done with the queue, you need to
use the free function destroy<Elem, Impl>()
to destroy the queue on
the free store via a base pointer. delete q
will only trigger the base
class destructor, skipping the derived class destructor that actually frees the
memory associated with the queue elements on the free store.
The typical use case of this situation is using a function that accepts a generic queue type via pointer.
// library code
template <typename Elem, template <typename> typename Impl>
void process(IQueue<Elem, Impl>* queue, ...);
// client code
IQueue<int, dsa::CircArrayQueue>* q = new dsa::CircArrayQueue<int> {};
process(q);
...
dsa::destroy(q); // GOOD: no memory leak
So, if you’re indeed a library developer writing such kind of functions, you’re advised to document the proper usage in your documentation.
Demo Programs#
Wanna see a queue and the algorithms in action? No problem. Follow the
installation instructions to build the project. You’ll find a few demo programs
in the bin/
subdirectory.
Implementing Your Own Queue#
By design, all queue types are subclasses of the same “abstract” class template
IQueue<Elem, Impl>
, which specifies the interface of a conventional queue
data structure. The class template definition of the Circular Array
Queue looks something like this:
template <typename Elem>
class CircArrayQueue : public IQueue<Elem, CircArrayQueue>;
Here, the template parameter Elem
specifies the queue element type, and
the implementation class template inherit from the “abstract” base class
template using the Curiously Recurring Template Pattern (CRTP), which is a
key to achieving static inheritance and polymorphism.
Let’s say you’re going to define a class template MyQueue<Elem>
to
implement the Queue ADT. Here is a blueprint to get you started:
namespace dsa {
template <typename Elem>
class MyQueue : public IQueue<Elem, MyQueue> {
friend class IQueue<Elem, MyQueue>;
public:
/* ctor, dtor, copy & move op= as appropriate */
/* member functions specific to your implementation if any */
private:
/* data members */
/* helper functions if any */
/* ### MANDATORY INTERFACE MATCHERS ### */
};
} // end namespace
The mandatory interface matchers are the private member functions that
actually realize the functionality of their respective parent class members in
IQueue<Elem, Impl>
. Let’s zoom in to see what’s expected there.
std::size_t size_() const noexcept;
bool empty_() const noexcept;
void iter_(std::function<void(Elem const&)>) const;
Elem& front_();
Elem const& front_() const;
void enqueue_(Elem const& elem);
void enqueue_(Elem&& elem);
void dequeue_();
template <typename... Args>
void emplace_(Args&&... args);
You’re advised to follow the coding style of the cppdsa-*
libraries to
implement them all in a separate .inl
file and then #include
it at the
end of the header file for the MyQueue<Elem>
class template.
This is it!