Educational Objectives: After completing this assignment the student should have the following knowledge, ability, and skills:
Operational Objectives: Create the generic container class fsu::Queue<T> that satisfies the interface requirements given below, along with an appropriate test harness for the class.
Deliverables: Three files queue.t, fqueue.cpp, and log.txt.
============================================= builds: [2 pts each] fqueue.x [student harness] x fqueue_int.x x fqueue_String.x x in2post.x x constTest.x x tests: [5 pts each] fqueue_char.x b < q.com1 [0..5]: x fqueue_int.x b < q.com2 [0..5]: x fqueue_String.x b < q.com3 [0..5]: x in2post.x b < i2p.in1 [0..5]: x in2post.x b < i2p.in2 [0..5]: x constTest.x [0..5]: x log.txt [-50..5]: x code quality [-50..5]: x dated submission deduction [2 pts each]: ( x) -- total [0..50]: xx =============================================
An abstract data type, abbreviated ADT, consists of three things:
The operations and axioms together should determine a unique character for the ADT, so that any two implementations should be essentially equivalent. (The word isomorphic is used to give precision to "essentially equivalent". We'll look at this in the next course.)
The queue ADT is used in many applications and has roots that pre-date the invention of high-level languages. Conceptually, queue is a set of data that can be expanded, contracted, and accessed using very specific operations. The queue ADT models the "FIFO", or first-in, first-out rule. The actual names for the queue operations may vary somewhat from one description to another, but the behavior of the abstract queue operations is well known and unambiguously understood throughout computer science. Queues are important in many aspects of computing, ranging from hardware design and I/O to inter-machine communication and algorithm control structures.
Typical uses of ADT Queue are (1) buffers, without which computer communication would be impossible, (2) control of algorithms such as breadth first search, and (3) simulation modelling of systems as diverse as manufacturing facilities, customer service, and computer operating systems.
The queue abstraction has the following operations and behavior:
As one example of the use of ADTs in computing, consider the following function that illustrates an algorithm for converting arithmetic expressions from infix to postfix notation:
... #include <queue.t> #include <stack.t> ... typedef fsu::Queue < Token > TokenQueue; typedef fsu::Stack < Token > TokenStack; // a Token is either an operand, an operator, or a left or right parenthesis ... bool i2p (TokenQueue & Q1, TokenQueue & Q2) // converts infix expression in Q1 to postfix expression in Q2 // returns true on success, false if syntax error is encountered { ... TokenStack S; // algorithm control stack Q2.Clear(); // make sure ouput queue is empty while (!Q1.Empty()) { // take tokens off Q1 and use them to build Q2 // if syntax error is detected return false } return true; } // end i2p()
This is a complex algorithm, but not beyond your capability to understand. The main points to displaying it here are: (1) to illustrate how stacks and queues may be used in implementing applications, and (2) to let you know that your code must be compatible with such apps. in2post.cpp is one of our tests!
We will implement the queue abstraction as a C++ class template Queue that contains the Queue ADT operations in its public interface.
Create and work within a separate subdirectory cop3330/proj8. Review the COP 3330 rules found in Introduction/Work Rules.
After starting your log, copy the following files from the course directory [LIB] into your proj8 directory:
proj8/in2post.cpp proj8/constTest.cpp proj8/deliverables.sh scripts/submit.sh # may be skipped if you have installed submit as a command area51/in2post_i.x area51/fqueue_i.x
Define and implement the class template fsu::Queue<T> in the file queue.t according to the RingBuffer implementation plan. Be sure to make log entries for your work sessions.
Devise a test client for Queue<T> that exercises the Queue interface (as well as the RingBuffer enhancements) for at least one native type and one user-defined type T. Put this test client in the file fqueue.cpp. Be sure to make log entries for your work sessions.
Test Stack and Queue using the supplied application in2post.cpp. Again, make sure behavior is appropriate and make corrections if necessary. Log your activities.
Turn in queue.t, fqueue.cpp, and log.txt using the submit.sh submit script.
Warning: Submit scripts do not work on the program and linprog servers. Use shell.cs.fsu.edu to submit projects. If you do not receive the second confirmation with the contents of your project, there has been a malfunction.
The term ringbuffer describes a specific type of implementation of the ADT queue.
ringbuffers are used in a number of applications, typically to store keyboard input temporarily "at" a terminal before a "send" command is issued. (The quotes are intended to convey that there is some ambiguity to the words inside them.) For example, if you establish a command prompt terminal to a Unix login using ssh, the operating system will need a way to store the characters you type as you type them prior to hitting the ENTER key. When ENTER is entered, this signals the OS to send the accumulated input to whatever application is expecting it.
Terminal support is just one of many applications of ringbuffers. They are useful just about anywhere an ADT queue is called for.
The ringbuffer interface is an ADT queue API, with some enhancements, as shown in the public sector of the class definition:
template < typename T > class Queue // as RingBuffer { public: // Queue API void Push (const T& t); T Pop (); T& Front (); const T& Front () const; size_t Size () const; bool Empty () const; void Clear (); // extra goodies void Append (const Queue& q); // append entire contents of q void Display (std::ostream& os, char ofc = '\0') const; // show queue (starting at front) size_t Capacity () const; // how many items can be stored using existing allocation void Release (); // de-allocate all links void Dump (std::ostream& os, char ofc = '\0') const; // show carrier ring (starting at front of queue) // proper type Queue (); Queue (const Queue&); ~Queue (); Queue& operator = (const Queue&); private: class Link { Link (const T& t) : element_(t), next_(nullptr){} T element_; Link * next_; friend class Queue; }; Link * firstUsed_; // head of queue Link * firstFree_; // one past last item in queue static Link* NewLink (const T& t); }; template < typename T > std::ostream& operator << (std::ostream& os, const Queue<T>& q) { q.Display(os, '\0'); return os; }
Note that we have implemented the output operator in terms of the Display method. The private sector of the class defined above gives hints toward an implementation.
The RingBuffer implementation plan uses a collection of links created dynamically to store data, one data item per link, each link pointing to a following link. This aspect of the implementation is analagous to that of a linked list such as fsu::List<>. However, there is no "first" or "last" link - the links form a cycle, or ring, hence the name "ringbuffer".
The RingBuffer implementation maintains two pointers into this ring structure, "firstUsed_", which points to the link containing the first item in the queue, and "firstFree_" which points to the first link that is not currently used to store queue data. Items are removed from the "front" of the queue by simply advancing the firstUsed_ pointer. Space is created for new items at the "back" of the queue by simply advancing the firstFree_ pointer. (There's an exception to this - see below.)
We refer to the underlying ring of links as the carrier ring. At any given time, the carrier ring must have at least one link more than the size of the queue in order to distinguish between the "empty queue" and "full carrier" states. The "queue support" consists of the links from firstUsed_ to (but not including) firstFree_.
States and values that need special treatment are:
null carrier: either or both queue pointers are zero empty queue: the carrier is null, or the first used link is the same as the first free link full carrier: the first free link is the only free link size: number of elements currently stored in the queue capacity: zero, or one less than the number of links in the carrier ring
Whenever the carrier is full, a Push(t) operation must create a new link for the new data item. However, Pop() operations do not release memory but just change the firstUsed_ pointer. In this way, the carrier maintains capacity equal to the largest size the queue has attained since it was created (or since the last call to Release()). Similarly, Clear() does not de-allocate memory but just resets queue pointers to the empty queue state. Only the Release() method actually reduces the size of the carrier. This conservation of created links during operations that decrease the size of the queue leads to runtime efficiency in situations where a queue may exist for a long time but always maintain a modest size.
A null carrier ring is detected by firstUsed_ == nullptr.
A full carrier ring is detected by firstFree_->next_ == firstUsed_ (i.e., there is only one free link).
Maintain the (non-null) carrier ring with at least one "unused" link.
There are three cases in implementing Push(t). If the carrier ring is null, two links must be created, one to hold the item and one to be free. If the carrier ring is full, one new (free) link must be created. Otherwise, all that is needed is to "advance" the firstFree_ pointer to the next link after assigning the data appropriately. To advance a link pointer p means going to the next link:
p = p->next_
An empty queue is detected by either a null carrier ring or firstFree_ == firstUsed_.
If the queue is not empty, Pop() should "advance" the firstUsed_ pointer, otherwise do nothing. Return the value that is "removed" from the queue.
Clear() does not change the carrier ring. Only the pointer member variables are changed to make the queue empty.
Release() actually de-allocates all of the dynamically allocated memory of the carrier ring and makes the carrier ring null. Release() is called by the destructor. It may also be called by client programs.
The assignment operator may be implemented with a call to either Release or Clear, followed by a call to Append. Using Release ensures that memory for the destination queue is just enough to hold a copy, whereas using Clear ensures that previously allocated memory is re-used.
Note that Link is defined only in the scope Queue<T>::. Also note that all members of class Link are private, which means a Link object can be created or accessed only inside an implementation of its friend class Queue<T>. (This prevents any client program from having access to a Link object.) The only method for class Link is its constructor, whose implementation should just initialze the two variables.
Append(q) just pushes the contents of q. It is useful in implementing the copy constructor and assignment operator.
The only data stored statically in a Queue object are the values of the two pointers firstUsed_ and firstFree_. These in turn provide access to the carrier ring, which consists of a collection of Link objects (aka "links") arranged in a circular manner using the "next" pointers in the links.
Queue should implement the class template Queue as defined under RingBuffer Interface and Ringbuffer Implementation Plan above.
The Queue constructor should create an empty queue with no dynamic memory allocation.
The Queue<T>::Push(t) operation operates in three modes, depending on the state of the carrier ring:
The Queue<T>::Pop() operation does not de-allocate memory, it just removes the front of the queue from consideration by advancing firstUsed_.
As always, the class destructors should de-allocate all dynamic memory still owned by the object. The stack and queue implementations will be very different.
Use the RingBuffer implementation plan discussed above, and implement the complete RingBuffer API.
The Display(os, ofc) method is intended to regurgitate the contents out through the std::ostream object os. The second parameter ofc is a single output formatting character that has the default value '\0'. (The other three popular choices for ofc are ' ', '\t' and '\n'.) The implementation of Display must recognize two cases:
The output operator should be overloaded as follows:
template < typename T > std::ostream& operator << (std::ostream& os, const Queue<T>& S) { S.Display (os, '\0'); return os; }
The overload of operator <<() should be placed in your queue header file immediately following the class definition.
The class Queue should be in the fsu namespace.
The file queue.t should be protected against multiple reads using the #ifndef ... #define ... #endif mechanism.
The test client program fqueue.cpp should adequately test the functionality of queue, including the output operator. It is your responsibility to create this test program and to use it for actual testing of your queue data structure.
Your test client can be created by making very small changes in a copy of fstack.cpp you created as part of the preceding assignment.
Keep in mind that the implementations of class template methods are in themselves template functions. For example, the implementation of the Queue method Pop() would look something like this:
template < typename T > T Queue<T>::Pop() { // yada dada return ??; }
We will test your implementation queue.t using (1) our test client fqueue.cpp and (2) in2post (with your stack implementation).
There are two versions of Queue::Front(). These are distunguished by "const" modifiers for one of the versions. The implementation code is identical for each version. The main point is that "Front()" can be called on a constant queue, but the returned reference may not be used to modify the front element. This nuance will be tested in our assessment. You can test it with two functions such as:
char ShowFront(const fsu::Queue<char>& q) { return q.Front(); } void ChangeFront(fsu::Queue<char>& q, char newFront) { q.Front() = newFront; }
Note that ShowFront has a const reference to a queue, so would be able to call the const version of Front() but not the non-const version, but that suffices. ChangeFront would need to call the non-const version in order to change the value at the front of the queue. A simple test named "constTest.cpp" is posted in the distribution directory.