Project 2: RingBuffer

Due: 02/27/05

Educational Objectives: Experience implementing and testing dynamic data structures using pointers; experience implementing and using linked structures, abstract lists, and abstract iterators

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 test_report.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                      value_type;
  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
  int       PushBack     (const T& t);  // push copy of t onto back of buffer (see note 1)
  int       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
  int       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 value_type;
  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
  T&  Retrieve       () const; // Return reference to current Tval 
  int Valid          () const; // Iterator may be dereferenced (see note 10)

  // various operators
  int        operator == (const Iterator& I2) const;
  int        operator != (const Iterator& I2) const;
  T&         operator *  () const; // Return 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 that retrieves the link data for the client and increments by going to the next link in the ring.

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

  // structural data                                                          (see note 9)
  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:
  // data
  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 us 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 usual 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 this->Empty() returns true.
  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 buffer carrier, 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.
    

    We could re-design class RingBufferIterator<> by adding a pointer data member

    RingBuffer<T> * myContainer;
    

    which would be maintained to point to the RingBuffer object that owns the carrier ring into which current points. Then we could define Valid() to return the value (current != (myContainer -> firstFree)), thus satisfying standard client expectations and the CAUTION could be removed. In either case, 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 nevertheless Valid() iterators. 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. Make sure you understand the implementation plan for RingBuffer<T> described above.

  3. Work within your subdirectory called cop4530/proj2. 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.

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

    LIB/submitscripts/proj2submit.sh
    LIB/tests/frbuff.cpp
    LIB/tests/fcqueue.cpp
    LIB/tests/mrbuff.cpp
    LIB/proj2/makefile
    
  5. 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 test_report.txt consisting of a report describing your testing results. Both files should be placed in the proj2 directory.

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

  7. Turn in the files ringbuff.cpp and test_report.txt using the script proj2submit.sh.

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