Project 8: Queue

A generic Queue data structure

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.

Assessment Rubric

=============================================
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
=============================================

Abstract Data Types

An abstract data type, abbreviated ADT, consists of three things:

  1. A set of elements of some type T
  2. Operations that may modify the set or return values associated with the set
  3. Rules of behavior, or axioms, that determine how the operations interact

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.)

Queues

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.

Abstract Queue Interface

The queue abstraction has the following operations and behavior:

Application: Converting Infix to Postfix Notation

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.

Procedural Requirements

  1. Create and work within a separate subdirectory cop3330/proj8. Review the COP 3330 rules found in Introduction/Work Rules.

  2. 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
    
  3. 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.

  4. 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.

  5. Test Stack and Queue using the supplied application in2post.cpp. Again, make sure behavior is appropriate and make corrections if necessary. Log your activities.

  6. 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.

RingBuffers

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.

RingBuffer Interface

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.

RingBuffer Implementation Plan

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.

Implementation Notes

  1. A null carrier ring is detected by firstUsed_ == nullptr.

  2. A full carrier ring is detected by firstFree_->next_ == firstUsed_ (i.e., there is only one free link).

  3. Maintain the (non-null) carrier ring with at least one "unused" link.

  4. 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_

  5. An empty queue is detected by either a null carrier ring or firstFree_ == firstUsed_.

  6. If the queue is not empty, Pop() should "advance" the firstUsed_ pointer, otherwise do nothing. Return the value that is "removed" from the queue.

  7. Clear() does not change the carrier ring. Only the pointer member variables are changed to make the queue empty.

  8. 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.

  9. 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.

  10. 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.

  11. Append(q) just pushes the contents of q. It is useful in implementing the copy constructor and assignment operator.

  12. 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.

Code Requirements and Specifications

  1. Queue should implement the class template Queue as defined under RingBuffer Interface and Ringbuffer Implementation Plan above.

  2. The Queue constructor should create an empty queue with no dynamic memory allocation.

  3. The Queue<T>::Push(t) operation operates in three modes, depending on the state of the carrier ring:

    1. When the carrier ring is null, two links are created with t in the link pointed to by firstUsed_.
    2. When the carrier ring is full, one new link is created, t is added to the queue by copying t into firstFree_ before advancing firstFree_.
    3. When the carrier ring has extra capacity, no new link is created and t is added to the queue by by copying t into firstFree_ before advancing firstFree_.

  4. The Queue<T>::Pop() operation does not de-allocate memory, it just removes the front of the queue from consideration by advancing firstUsed_.

  5. 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.

  6. Use the RingBuffer implementation plan discussed above, and implement the complete RingBuffer API.

  7. 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:

    1. ofc == '\0': send the contents to output with nothing between them
    2. ofc != '\0': send the contents to output with ofc separating them
    Thus, for example, q.Display(std::cout) would send the contents of q to standard output.

  8. 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.

  9. The class Queue should be in the fsu namespace.

  10. The file queue.t should be protected against multiple reads using the #ifndef ... #define ... #endif mechanism.

  11. 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.

Hints