Project 1: Circular Buffer of Circular Buffers

Due: Tuesday, September 25, 8:59:59pm

Links: [Project Submission] [Late Submissions] [Project Grading] [Grading Guidelines] [Academic Conduct]

Objectives

The objective of this programming assignment is to have you review C++ programming using following the features: object-oriented design, dynamic memory allocation (of arrays) and pointer manipulation.

Introduction

Note: Circular Buffers are described in Section 5.2.4 of our textbook (p. 211). Please read that section before proceeding.

Two common data structures for implementing First-In-First-Out (FIFO) queues are singly-linked lists and arrays used as circular buffers. The advantage of using an array is that we can add and remove items from the FIFO queue quickly simply by updating the indices of the start and the end of the queue. As items are added and removed from the array, the indices would "wrap around" and space in the array is reused. In the example below, 14 is at the start of the queue and 21 is at the end. If we added another item to the FIFO queue, it would go in index #1. If we remove an item from the FIFO queue, 14 would be removed.

When using an array for a circular buffer, we must make sure that the array is big enough to hold the largest number of items that can be in the FIFO queue simultaneously. Suppose we added two more values–22 and 23–to the circular buffer above. Then, the buffer would be full:

We cannot add another item to the circular buffer. What do we do? (There is a version of circular buffers that allow the data structure to drop the oldest item in the queue if the buffer is full. This is useful in some applications. For example, if we use the circular buffer to store the last 5 seconds of an audio recording, we can just "erase" the oldest part of the recording and reuse the space for the newest. For this project we are not allowed to drop any data.)

The standard trick when we run out of space in an array is to make a new array that holds twice as much data and copy the data over, freeing the old array afterwards. Copying is slow, though. So, instead of copying, we will just add another array for new values, and keep both arrays. (Note that we are still using the strategy of allocating a new array that is twice as big.) The following diagram shows what happens if we add 24, 25, 26, 27, 28 and 29 to the example above.

The resulting situation is slightly more complex than a simple expanded single array. We now have to remember that when we remove something from the FIFO queue, we remove it from the start of the old array. but when we add something to the FIFO queue, we add it to the end of the new array. For example, if we added 30, 31 and 32 and removed two items from the queue above, then we would have:

In a simple world, eventually, all of the items in the old array will be removed and we would be left with just the new array. We then fall back to the "normal" circular buffer implemented as a single array. For example, if we removed 11 items from the figure above, we would get:

But, what if we add lots of items to the FIFO queue and even the second array fills up? Then, we just create a third array. What if that one becomes full? Then we create another one. And if that one is full? In the general case, we have a bunch of arrays. We keep track of which array is the oldest and which one is the newest. We always add to the newest array and remove from the oldest array:

Once the oldest array is emptied, we can deallocate that array, and we remove items from the next oldest array. If the newest array is full, then we create another with twice the capacity. We use an array of pointers to keep track of these circular buffers. In the general case, the array of pointers might also wrap around, because (yes, you guessed it) the array of pointers (in blue below) is itself a circular buffer:

What if eventually, the outer circular buffer is full, too? In that situation, your initial estimate of the number of items that can simultaneously be in your FIFO queue would have to be wrong by a huge factor. While there are many possible solutions there, too, for this project, we will just give up and throw an exception.

Interesting Observation

The limited version of the data structure we are implementing here, where the outer buffer is fixed at some size (in our case, 7), displays an interesting behavior: the ultimate capacity before it throws an exception depends on the exact usage pattern. Imagine a toy case where we limit our outer circular buffer to size 3, and start our first inner buffer at size 1. If the user starts right off doing nothing but enqueues, the whole data structure will top out at 7 items before throwing an exception on the 8th addition. However, if the user enqueues 7 items, but then dequeues them all, it will cause all but the newest–and largest, at size 4–inner buffer to be deallocated. Then, if the user starts enqueuing again, after 4 items are added, another inner buffer will be allocated with size 8, and a third with size 16 after that if the user keeps enqueuing. At that point, our total capacity would be 28. You can see that the implementation we are doing for this project is therefore overly sensitive to the exact growth pattern of the user's storage needs, which is not a good characteristic of a data structure. Luckily, you are not planning to try to sell this system commercially...

Assignment

Your assignment is to implement the "circular buffer of circular buffers" data structure described above. For this project, you will have very limited design choices. You are required to use the class definitions given in InnerCB.h and CBofCB.h. (Specifications are given below.) The InnerCB class is an implementation of a simple circular buffer of int values. (Like the green arrays above.) The CBofCB class is the circular buffer of circular buffers.

// file: InnerCB.h // // UMBC CMSC 341 Fall 2018 Project 1 // // Header file for Inner Circular Buffer. // See project description for details. // #ifndef _INNERCB_H_ #define _INNERCB_H_ class InnerCB { public: // Constructor, default size is 10. InnerCB(int n=10) ; // Copy constructor InnerCB(const InnerCB& other) ; // Destructor ~InnerCB() ; // Add item to circular buffer void enqueue(int data) ; // Remove item from circular buffer int dequeue() ; // True if no space left in buffer bool isFull() ; // True if buffer holds no items bool isEmpty() ; // return maximum number of items this buffer can hold int capacity() ; // return number of items currently held in the buffer int size() ; // overloaded assignment operator const InnerCB& operator=(const InnerCB& rhs) ; // debugging function. Prints out contents. void dump() ; // grading function used to examine private data members. // Do not implement! bool inspect (int* &buf, int &cap, int &size, int &start, int &end) ; private : int *m_buffer ; // pointer to dynamically allocate array for buffer int m_capacity ; // length of the allocated space pointed by m_buffer int m_size ; // # of items in the buffer int m_start ; // index of the first (oldest) item in the buffer int m_end ; // index of the last (newest) item in the buffer } ; #endif


// file: CBofCB.h // // UMBC CMSC 341 Fall 2018 Project 1 // // Header file for Circular Buffer of Circular Buffer. // See project description for details. // #ifndef _CBOFCB_H_ #define _CBOFCB_H_ #include "InnerCB.h" class CBofCB { public: // default constructor CBofCB() ; // copy constructor CBofCB(const CBofCB& other) ; // destructor ~CBofCB() ; // add item to this data structure void enqueue(int data) ; // remove item from this data structure int dequeue() ; // returns true if cannot add more items bool isFull() ; // returns true if no items stored in data structure bool isEmpty() ; // number of items in the data structure as a whole. // Note: not the number of InnerCB's int size() ; // overloaded assignment operator const CBofCB& operator=(const CBofCB& rhs) ; // debugging function, prints out contents of data structure void dump() ; // grading function. Do not implement! bool inspect (InnerCB** &buf, int &cap, int &size, int &start, int &end) ; private : // max number of Inner Circular Buffers // static const int m_obCapacity=7 ; // array of pointers to InnerCB's. // Each entry of the array is a pointer. // Note: array itself is NOT dynamically allocated InnerCB * m_buffers[m_obCapacity] ; int m_obSize ; // number of inner circular buffers in the outer CB int m_oldest ; // index of the oldest circular buffer (start) int m_newest ; // index of the newest circular buffer (end) } ; #endif


Specifications

Here are the specifics of the assignment, including a description for what each member function must accomplish.

Requirement: Place the implementation of the InnerCB member functions in a file called InnerCB.cpp and the implementation of the CBofCB member functions in CBofCB.cpp.

Requirement: All member functions must be implemented from scratch. In particular, you are not allowed to use any classes from the Standard Template Library (STL), not even vector.

Requirement: You are not allowed to add anything or remove anything from these header files. Your submission must compile with the original .h files without modification.

Requirement: Do not use global variables to circumvent the prohibition from altering InnerCB.h and CBofCB.h. Actually, just don't use global variables period.

Requirement: Your code must compile on GL with the exact Unix commands given below. (See "How to Submit".)

Requirement: Your code must not have any memory leaks. When you run your code under valgrind on GL, it must report:

All heap blocks were freed -- no leaks are possible

Requirement: Your implementation must be efficient. Tests p1test11.cpp, p1test12.cpp and p1test13.cpp should take approximately 16, 32 and 64 seconds of user time to run on GL. The actual number of seconds do not matter as much as the fact that each program takes about twice as long as the previous one. This shows that your implementation is taking linear time.



These are the member functions of the InnerCB class:



These are the member functions of the CBofCB class:


Test Programs

The following test programs should be used to check the compatibility of your implementation. These programs do not check the complete correctness of your implementation. Even if your implementation compiles and runs correctly with these programs, it does not mean your implementation is error-free. Grading will be done using different programs.

In general, you must do the testing yourself --- testing is part of programming. This is a more comprehensive set of test programs than we will provide in subsequent projects. For the first project, we are trying to show you the minimum amount of testing you should be doing.

Conversely, if your implementation does not compile or does not run correctly with these test programs, then it is unlikely that it will compile or run correctly with the grading programs.

These files are also available on GL in the directory:

/afs/umbc.edu/users/p/a/park/pub/cs341/00Proj1/


Implementation Notes


How to Submit

You must submit the following files to the proj1 directory.

You do not need to submit InnerCB.h or CBofCB.h because it should not have changed.

If you followed the instructions in the Project Submission page to set up your directories, you can submit your code using this Unix command:

cp InnerCB.cpp CBofCB.cpp ~/cs341proj/proj1/

Use the Unix script command to show that your code compiles:

linux1% cd ~/cs341proj/proj1/ linux1% script Script started, file is typescript linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test01.cpp InnerCB.cpp -o t01.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test02.cpp InnerCB.cpp -o t02.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test03.cpp InnerCB.cpp -o t03.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test04.cpp InnerCB.cpp -o t04.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test05.cpp InnerCB.cpp -o t05.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test06.cpp InnerCB.cpp CBofCB.cpp -o t06.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test07.cpp InnerCB.cpp CBofCB.cpp -o t07.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test08.cpp InnerCB.cpp CBofCB.cpp -o t08.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test09.cpp InnerCB.cpp CBofCB.cpp -o t09.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test10.cpp InnerCB.cpp CBofCB.cpp -o t10.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test11.cpp InnerCB.cpp CBofCB.cpp -o t11.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test12.cpp InnerCB.cpp CBofCB.cpp -o t12.out linux1% g++ -I ../../00Proj1 ../../00Proj1/p1test13.cpp InnerCB.cpp CBofCB.cpp -o t13.out linux1% exit exit Script done, file is typescript

Do remember to exit from the script command. This creates a file called typescript that will record any compilation errors. Yes, we know you can edit this file, but the compilation errors will just show up when we compile the programs again and you will still get lots of points deducted. This step is to compel you to fix any changes needed to get your program to compile on GL without any errors.

Note: cd to the appropriate alternate directory if you are submitting late.

Run t01.out thru t10.out under valgrind:

linux1% valgrind ./t01.out ... linux1% valgrind ./t02.out ... linux1% valgrind ./t10.out

If you do not see

All heap blocks were freed -- no leaks are possible

after each run, then you have a memory leak. You are not ready to submit. Go back and fix your program. If valgrind reports other memory errors (e.g., reading from invalid memory or writing to invalid memory) then you don't have a memory leak, but you have some other sort of bug. You should also go back and fix your program.

(Note: do not run t11.cpp, t12.cpp or t13.cpp under valgrind: they take too long.)

Finally, use the script command to record your timing runs for p1test11.cpp, p1test12.cpp and p1test13.cpp. We want to use a separate file to record this, so use the -a option for script:

linux1% script -a timing.txt Script started, file is timing.txt linux1% time ./t11.out ... Total of 335,544,310 items added and removed 17.722u 0.676s 0:18.41 99.8% 0+0k 0+0io 0pf+0w linux1% time ./t12.out ... Total of 2 * 335,544,310 items added and removed 34.421u 0.504s 0:34.96 99.8% 0+0k 0+0io 0pf+0w linux1% time ./t13.out ... Total of 4 * 335,544,310 items added and removed 68.571u 1.798s 1:10.43 99.9% 0+0k 0+0io 0pf+0w linux1% exit exit Script done, file is timing.txt

Output from the programs are omitted above. The 17.722u, 34.421u and 68.571u are the user times from running the three programs.

Now you can delete the executable files with

rm t??.out

Then you should just have 4 files in your submission directory. Check using the ls command. You can also double check that you are in the correct directory using the pwd command. (You should see your username instead of xxxxx.)

linux1% ls CBofCB.cpp InnerCB.cpp timing.txt typescript linux1% pwd /afs/umbc.edu/users/p/a/park/pub/cs341/xxxxx/proj1 linux1%