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
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.)
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 ... };
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
- 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.
- If the buffer is not empty, PopFront() should "increment" the firstUsed_ pointer, otherwise do nothing. Return 1/0 depending on success.
- Clear() does not change the carrier ring. Only the buffer pointer(s) are changed to make the buffer empty.
- 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.
- Returns an iterator pointing to the beginning of the buffer
- 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.
- 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.)
- 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.
- 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.
- 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.
Begin with understanding the chapters on Lists and Deques and a working knowledge of the techniques involved in creating linked structures dynamically.
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.
Make sure you understand the implementation plan for RingBuffer<T> described above.
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.
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
Create two more files:
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.
Turn in the files ringbuff.cpp and log.txt using the script proj1submit.sh.
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.
Your file ringbuff.cpp is a "slave" file to ringbuff.h. (See the chapter on Vectors for an explanation of "slave" file.)
Your implementation should follow the plan detailed above.
The method Dump() should illustrate the internal structure of the object, including the carrier ring and the first pointers.
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.
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.
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.
The following files are used directly from the course library:
tcpp/ringbuff.h // defines fsu::RingBuffer<T> and fsu::RingBufferIterator<T> cpp/xran.h // the fsu::xran family of random object generators cpp/xran.cpp // ... these are used by mringbuff.cpp cpp/xranxstr.h cpp/xranxstr.cpp
The file tcpp/ringbuff.h is in the course library. Note that the file ringbuff.cpp is included into this file near the bottom but inside the namespace fsu. Therefore the code in ringbuff.cpp is automatically in the correct namespace.
The file frbuff.cpp contains a typical "functionality" test program. The idea is to provide access to the entire public interface of class RingBuffer<> and RingBufferIterator<> so that you can perform operations on three distinct RingBuffer objects and associated iterators. It is up to you to use this test program effectively.
The file mrbuff.cpp contains a dynamic test for correct memory and pointer management in the implementation of RingBuffer<> and RingBufferIterator<>. In contrast to the functionality test, this one runs without user input. It sets up three RingBuffer<> objects and associated iterators, much as is done in frbuff, but the operations are called randomly in a loop that runs until Ctrl-C is entered or until the program crashes. This is a very dangerous program that will crash the entire server on which it runs if a defective implementation of RingBuffer is tested without careful containment of the runspace for the program. Therefore it is imperative that the precautions delineated in the documentation be followed.
The file fcqueue.cpp contains a functionality test for the CQueue<> adaptor class. Run this test using RingBuffer as a basis for ADT queue to be sure that your RingBuffer implements queue correctly.
A makefile for your project that compiles separate executables for three client programs is supplied. You can compile the two supplied test programs using this makefile by entering the command "make two". You can compile individual test programs or any other target in the makefile by entering "make xxx" where "xxx" is the target. For example, "make frbuff.x" creates the executable for frbuff.cpp and "make mrbuff.o" creates the object code for mrbuff.cpp.
Be sure to follow the recommendations in Chapter 1 on incremental implementation of complex classes. In particular: create the file ringbuff.cpp with all method headers and minimalist non-functional bodies (empty or returning a simple value of the appropriate type). This should result in no compile or link errors. Then proceed to implement the methods one at a time. Test the methods as you build them. Think about, understand, plan, and design each method before proceeding to its implementation.
Sample executables for fcqueue, frbuff, and mrbuff are available in LIB/area51.