Project 1: RingBuffer

Educational Objectives: After completing this assignment, the student should be able to accomplish the following:

Operational Objectives: Supply source code implementing the template classes RingBuffer<T> and RingBufferIterator<T>. Code should be thoroughly tested for functional correctness, robustness, and memory management. The supplied code should function correctly and be free of memory leaks, and your tests should provide evidence of both. This evidence should be summarized in a test report.

Deliverables: Two files: ringbuff.cpp and log.txt

Ring Buffers

A ring buffer is a specific type of implementation of the ADT queue with an associated iterator class. It is unusual, and somewhat out of sync with the notion of ADT, to imply details about the implementation in the descriptive name of a structure, but tradition calls for an exception in this case.

Ring buffers 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 applications is expecting it.

Terminal support is just one of many applications of ring buffers. They are useful just about anywhere an ADT queue is called for. Note, however, that ring buffer behavior differs from our concept of deque in that it has no bracket operator or other means to access elements except at the ends, and it differs from an abstract queue in that it has iterators that can traverse the structure sequentially, pause, and access data stored at a location.

Typical ringbuffers use dynamically allocated memory in some way so that there is an expandable buffer that doesn't artificially limit the size of the input. (There may be size limits imposed externally, for policy reasons.)

RingBuffer Interface

The ring buffer will be an implementation of ADT queue with iterator support and modified nomenclature expressed in the following public interfaces:

template < typename T >
class RingBuffer
{
  friend class RingBufferIterator<T>;

public:
  // terminology support:
  typedef T                      ValueType;
  typedef RingBufferIterator<T>  Iterator;

  // proper class:
  RingBuffer             ();                  // parameterless ("default") constructor
  ~RingBuffer            ();                  // destructor
  RingBuffer             (const RingBuffer&); // copy constructor
  RingBuffer& operator = (const RingBuffer&); // assignment operator

  // modifying the buffer
  bool      PushBack     (const T& t);  // push copy of t onto back of buffer (see note 1)
  bool      PopFront     ();            // pop front of buffer                (see note 2)
  void      Clear        ();            // empty the buffer                   (see note 3) 
  void      Release      ();            // release buffer memory              (see note 4)

  // information about the buffer
  size_t    Size         () const;      // return the number of elements in the buffer
  size_t    Capacity     () const;      // return the number of memory positions available
  bool      Empty        () const;      // true iff buffer has no elements

  // accessing values in the buffer
  T&        Front        ();            // return reference to Tval at front of buffer
  const T&  Front        () const;      // const version

  // locating places on the buffer
  Iterator  Begin        () const; // return iterator to first                (see note 5)
  Iterator  End          () const; // return iterator to one past the last    (see note 6)

  // show picture of the internal structure for development/testing purposes
  void Dump              (std::ostream& os, char ofc = '\0') const;
  ...
};

template <typename T>
class RingBufferIterator
{
friend class RingBuffer<T>;

public:
  // terminology support
  typedef T                      ValueType;
  typedef RingBufferIterator<T>  Iterator;

  // constructors
  RingBufferIterator ();                        // default constructor
  RingBufferIterator (const RingBuffer<T>& b);  // initialize to front of buffer b
  RingBufferIterator (const Iterator& i);       // copy constructor

  // information/access
  const T&   Retrieve    () const; // Return const reference to current Tval 
  int        Valid       () const; // Iterator may be dereferenced (see note 10)

  // various operators
  bool       operator == (const Iterator& i2) const;
  bool       operator != (const Iterator& i2) const;
  const T&   operator *  () const; // Return const reference to current Tval
  Iterator&  operator =  (const Iterator <T> & i);
  Iterator&  operator ++ ();       // prefix
  Iterator   operator ++ (int);    // postfix
  ...
};

RingBuffer Implementation Plan

The implementation of RingBuffer 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 TList<>. However, there is no "first" or "last" link - the links form a cycle, or ring, hence the name "ringbuffer". The ring structure, on the other hand, is very similar to the circular array implementation of a TDequeue<T>, except that the data is contained in links instead of contiguous blocks of memory.

The RingBuffer object maintains two pointers into this ring structure, "firstUsed_", which points to the link containing the first item in the buffer, and "firstFree_" which points to the first link that is not currently used to store buffer data. Items are removed from the "front" of the buffer by simply iterating the firstUsed_ pointer. Space is created for new items at the "back" of the buffer by simply iterating 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 buffer in order to distinguish between the "empty buffer" and "full carrier" states. The "buffer support" consists of the links from firstUsed_ to (but not including) firstFree_. "Buffer" is an ADT equivalent to "queue".

States that need special treatment are:

null carrier:  either or both buffer pointers are zero
empty buffer:  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 buffer
capacity:      zero, or one less than the number of links in the carrier ring

Whenever the carrier is full, a PushBack(t) operation must create a new link for the new data item. However, PopFront() operations do not release memory but just change the firstUsed_ pointer. In this way, the carrier may have much higher capacity than the actual size of the buffer. Similarly, Clear() does not release memory but just resets buffer pointers to the empty buffer 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 buffer is analagous to the circular array.

A RingBufferIterator is just a protected pointer to a link. An iterator is used by client programs (1) to iterate through a RingBuffer using the increment operator ++ and to retrieve the link data for the client.

template < typename T >
class RingBuffer
{
...
private:

  class BLink // class used only by RingBuffer objects and  iterators         (see note 7)
  {
    friend class RingBuffer<T>;
    friend class RingBufferIterator<T>;

    // BLink varliables
    T        value_;          // where T data is stored
    BLink *  next_;           // ptr to successor Link

    // BLink constructor - parameter required
    BLink(const T& Tval) : value_(Tval), next_(0) {}
  } ; // class BLink

  // private method
  void Clone (const RingBuffer&); // too dangerous for public use             (see note 8)

  // RingBuffer variables
  BLink * firstUsed_,
        * firstFree_;

} ; // class RingBuffer<>

// NOTE: push, pop, clear, and release operations may make iterators illegitimate!
// It is a client responsibility to manage iterators correctly
// when ringbuffer structure is modified.

template <typename T>
class RingBufferIterator
{
...
private:
  // variable
  typename RingBuffer<T>::BLink  * current_;

  // operators not supported
  Iterator&  operator -- ();    // prefix
  Iterator   operator -- (int); // postfix

} ; // class RingBufferIterator<>

Implementation Notes

  1. There are three cases in implementing PushBack(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 "increment" the firstFree_ pointer to the next link after assigning the data appropriately. Return 1/0 depending on success.
  2. If the buffer is not empty, PopFront() should "increment" the firstUsed_ pointer, otherwise do nothing. Return 1/0 depending on success.

  3. Clear() does not change the carrier ring. Only the buffer pointer(s) are changed to make the buffer empty.
  4. Release() actually releases all of the dynamically allocated memory of the carrier ring and makes the carrier ring null. Release() is called by the destructor and the assignment operator. It may also be called by client programs.
  5. Returns an iterator pointing to the beginning of the buffer
  6. Returns an iterator pointing to "one past the end" of the buffer. Note that firstFree_ points to the link one past the last buffer element, so End() can return the iterator with current_ = firstFree_. Also note that this is different from the expectation that the End() iterator is "null". Because there are no zero "next" pointers in a non-null carrier ring, End() -> current_ has a non-zero value.
  7. Note that BLink is defined only in the scope RingBuffer<T>::. Also note that all members of class BLink are private, which means a BLink object can be created or accessed only inside an implementation of its friend classes RingBuffer<T> and RingBufferIterator<T>. (This prevents any client program from having access to a BLink object.) The only method for class BLink is its constructor, whose implementation should just initialze the two variables. (This can be done inside the class definition, as shown.)
  8. Clone(b) is a dangerous operation and is therefore not public. Clone(b) is used to make *this into an exact copy of b. Clone(b) is called in the implementation of both the copy constructor and the assignment operator. It must never be called unless the carrier ring of the calling object is empty.
  9. The only data stored statically in a RingBuffer 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 BLink objects (aka "links") arranged in a circular manner using the "next" pointers in the links.
  10. In any container class / iterator class design, care must be taken to ensure that standard behavior expectations are implemented or to inform client programmers of anamolies in behavior. In particular, client programs may have an expectation that the two loops

    for (RingBufferIterator<T> i = B.Begin(); i != B.End(); ++i)
    {}
    

    and

    for (RingBufferIterator<T> i = B.Begin(); i.Valid(); ++i)
    {}
    

    are equivalent. That is, the two statements

    i == End();
    

    and

    !i.Valid();
    

    are equivalent. The first of these is implemented as part of the container class, and the second is implemented as part of the iterator class. In the case of RingBuffer<> and RingBufferIterator<> as defined above this cannot be achieved: our only option is to define Valid() to return (current != 0). Then we put the following where client programmers will see it:

    CAUTION: RingBuffer<>::End() and RingBufferIterator<>::Valid() are not
    compatible. Valid() indicates a NULL iterator while End() returns a non-NULL
    iterator to one past the end of the buffer.
    

    Caution must be used by client programs, because there are other non-zero values, such as "two" past the end, that an Iterator may have that do not represent legitimate buffer items but are neither !Valid() nor the End() iterator. For this reason, the RingBuffer::Iterator is somewhat delicate in behavior and can yield unpredictable results when not handled carefully by client programs, for example by using an iterator without re-initializing after a buffer has been modified.

Procedural Requirements

  1. Begin with understanding the chapters on Lists and Deques and a working knowledge of the techniques involved in creating linked structures dynamically.

  2. The official development/testing/assessment environment is: gnu g++ 4.1.2 (Red Hat 4.1.2-42). This is the environment on the linprog machines.

  3. Make sure you understand the implementation plan for RingBuffer<T> described above.

  4. Work within your subdirectory called cop4530/proj1. Keep in mind that with all assignments it is a violation of course policy and the FSU Honor Code to give or receive help on assignments from anyone other than the course instruction staff or to copy code from any source other than those explicitly distributed in the course library.

  5. Copy the following files from the course LIB into your project directory:

    LIB/submitscripts/proj1submit.sh
    LIB/tests/frbuff.cpp
    LIB/tests/fcqueue.cpp
    LIB/tests/mrbuff.cpp
    LIB/proj1/makefile
    
  6. Create two more files:

    1. a source code file ringbuff.cpp implementing the template classes fsu::RingBuffer<T> and fsu::RingBufferIterator<T> that are defined in tcpp/ringbuff.h;
    2. a text file log.txt consisting of a log of all development activity, including documentation for all testing. Both files should be placed in the proj1 directory.

  7. Keep detailed notes on procedures and results as you test your implementation ringbuff.cpp. Use these testing notes to create a report in the file log.txt.

  8. Turn in the files ringbuff.cpp and log.txt using the script proj1submit.sh.

  9. Warning: Submit scripts do not work on the program and linprog servers. Use shell.cs.fsu.edu to submit this assignment. If you do not receive the second confirmation with the contents of your project, there has been a malfunction.

Technical Requirements and Specifications

  1. Your file ringbuff.cpp is a "slave" file to ringbuff.h. (See the chapter on Vectors for an explanation of "slave" file.)

  2. Your implementation should follow the plan detailed above.

  3. The method Dump() should illustrate the internal structure of the object, including the carrier ring and the first pointers.

  4. Use the scope fsu::RingBuffer<T>:: version of the link class that is declared in ringbuff.h. Do not declare any new classes in the implementation file.

  5. Your implementation of RingBuffer<T> and RingBufferIterator<T> should be tested for both functionality and memory containment using at least the classes T = char and T = fsu::String. Three test programs, clients of fsu::RingBuffer<T>, are supplied. Specific instructions for testing for memory leaks are included as comment at the top of the file mrbuff.cpp. DO NOT TEST FOR MEMORY LEAKS WITHOUT FOLLOWING THESE INSTRUCTIONS.

  6. Your report should not exceed one typed page when printed. It should begin with an introduction and brief description of what you are testing and how the tests were performed. The description should be written so that a person not familiar with this class would understand what the report is about and how to interpret the results of the tests. The results of the tests should follow the introduction. The results may be summarized in a table. The report should be a plain ascii text file.

Hints: