Lab 04 - Iterator
(02/09/2022)
Description
"The concept of an iterator is fundamental to understanding the C++ Standard Template Library (STL). Iterators provide access to data stored in container classes (e.g. vector, map, list, deque, etc.)
Add a nested iterator class to your linked list to sequentially access list elements. Your iterator supports begin() and end() functions along with overloaded not equal ("!="), dereferencing ("*"), and pre-incrementing ("++") operators.
Use the list iterator to iterate, find, insert after, replace, and erase elements in the linked list."
** Disclaimer: Lab 04 requires a minimal implementation of the Lab 03: Linked List **
Learning Outcomes
By completing the Iterator Lab, you will have:
- Added a nested iterator class to your linked list container.
- Overloaded class operators.
- Used iterator to iterate, find, insert after, replace, and erase linked list elements.
- Implemented toString and friend class methods.
Discussion
Iterators.(collapse)
An iterator is basically a pointer to an element in a container and is used to access elements inside a container without exposing the structure of the container. An iterator is an object that shares the associated container data.
A container iterator pointing to the beginning of the container (the first element) is returned by the begin() function. The end() function returns an iterator corresponding to having reached the end of the container. All container iterators support, as a minimum, a dereference operator ("*") to read/modify elements in the container, a comparison operator ("!=") of iterators, and an increment operator("++") to point to the next element.
There are three different types of iterators that support different operations depending upon the container. For example, an iterator for a singly linked list supports "forward" movement, but not "backward" movement. An iterator for an array class container (doubly linked lists and binary trees) supports movement in both directions, and constant time addition (random access).
Nested Classes.(collapse)
Classes defined inside another class are called nested classes. Nested classes are used in situations where the nested class has a close conceptual relationship to its surrounding class.
A class can be nested in any part of the surrounding class: in the public, protected or private sections, and is considered a member of the surrounding class. The normal access rules of classes apply to nested classes. If a class is nested in the public section of a class, it is visible outside the surrounding class. Likewise, if nested in the protected section, it is visible in subclasses, derived from the surrounding class and if nested in the private section, it is only visible for the members of the surrounding class. The surrounding class has no special privileges towards the nested class. The nested class has full control over the accessibility of its members by the surrounding class.
Since iterators are closely associated with the container upon which it iterates, iterators are most often implemented as nested classes in the public member section of the container. The container begin() and end() member functions instantiate and return a class iterator object.
Prefix / Postfix Unary Operators.(collapse)
The prefix and postfix versions of the unary increment and decrement operators can all be overloaded. To allow both unary prefix and postfix increment usage, each overloaded operator function must have a distinct signature (so that the compiler can determine which version of "++" is intended.)
When the compiler sees the pre-increment expression on a Data data type:
Data myData; ++myData;
the compiler generates the member function call:
Data myData; myData.operator++();
The prototype for this operator function would be:
Data& operator++();
Overloading the postfix increment operator presents a challenge because the compiler must be able to distinguish between the signatures of the overloaded prefix and postfix increment operator functions.
The convention that has been adopted in C++ is that when the compiler sees the post-incrementing expression object++, it generates the member function call:
myData.operator++(0)
The argument 0 is strictly a "dummy value" that enables the compiler to distinguish between the prefix and postfix increment operator functions.
In summary:
// prefix (return by reference) MyClass& operator++() { // implement increment logic, return reference to it. return *this; } // postfix (return by value) MyClass operator++(int) // the dummy int disambiguates { MyClass tmp(*this); operator++(); // prefix-increment this instance return tmp; // return value before increment }
**NOTE: The return value of the overloaded postfix operator must be a non-reference, because we can’t return a reference to a local variable that will be destroyed when the function exits. Postfix operators are typically less efficient than the prefix operators because of the added overhead of instantiating a temporary variable and returning by value instead of reference.
C++ Iterators.(collapse)
The concept of an iterator is fundamental to understanding the C++ Standard Template Library (STL). Iterators provide a means for accessing data stored in container classes such a vector, map, list, etc.
Iterators are objects acting like pointers. You can think of an iterator as pointing to an item that is part of a larger container of items. All iterated containers support a function called "begin", which returns an iterator object pointing to the beginning of the container (the first element) and a function "end", which returns an iterator object corresponding to having reached the end of the container. You then can access the element by "dereferencing" the iterator with a "*", just as you would dereference a pointer.
The linked list iterator functions used in Lab 04 are as follows:
- Iterator begin();
Returns an iterator pointing to the first element in the list container.
Example:
LinkedList
::Iterator iter = linked_list.begin(); - Iterator end();
Returns an iterator referring to the past-the-end element in the list container. The past-the-end element is the theoretical element that would follow the last element in the list container. It does not point to any element, and thus shall not be dereferenced. Because the ranges used by functions of the standard library do not include the element pointed by their closing iterator, this function is often used in combination with list::begin to specify a range including all the elements in the container.
If the container is empty, this function returns the same as list::begin.
Example:
LinkedList<string>::Iterator iter = linked_list.begin(); while (iter != linked_list.end()) { cout << endl << " [" << *iter << "]"; ++iter; }
- Iterator find(Iterator first, Iterator last, const T& value);
Returns an iterator to the first element in the range [first,last) that compares equal to value. The range searched is [first,last), which contains all the elements between first and last, including the element pointed by first but not the element pointed by last. If no elements match, the function returns last.
Example:
LinkedList
::Iterator position; position = linked_list.find(linked_list.begin(), linked_list.end(), value); - Iterator insert(Iterator position, const T& value);
The container is extended by inserting a new element before the element at the specified position. The function returns an iterator pointing to the newly inserted element.
Example:
LinkedList
::Iterator position; position = linked_list.find(linked_list.begin(), linked_list.end(), find_value); linked_list.insert(position, before_value); - Iterator insert_after(Iterator position, const T& value);
The container is extended by inserting a new element after the element at the specified position. The function returns an iterator pointing to the newly inserted element.
Example:
LinkedList
::Iterator position; position = linked_list.find(linked_list.begin(), linked_list.end(), find_value); linked_list.insert_after(position, after_value); - Iterator erase(Iterator position);
Removes from the list container a single element (position) and returns an iterator pointing to the element that followed the element erased by the function call. This is the container end if the operation erased the last element in the sequence.
Example:
LinkedList
::Iterator position; position = linked_list.find(linked_list.begin(), linked_list.end(), value); linked_list.erase(position); - void replace(Iterator first, Iterator last, const T& old_value, const T& new_value);
Assigns new_value to all the elements in the range [first,last) that compare equal to old_value. The function uses operator== to compare the individual elements to old_value.
Example:
linked_list.replace(linked_list.begin(), linked_list.end(), "too", "very");
Summary
So why use iterators? First, they're a flexible way to access the data in containers that don't have obvious means of accessing all of the data (for instance, maps). They're also quite flexible - if you change the underlying container, it's easy to change the associated iterator so long as you only use features associated with the iterator supported by both classes. Finally, the STL algorithms defined in <algorithm> use iterators.
The Good:
- The STL provides iterators as a convenient abstraction for accessing many different types of containers.
- Iterators for templated classes are generated inside the class scope with the syntax class_name
::iterator. - Iterators can be thought of as limited pointers (or, in the case of random access iterators, as nearly equivalent to pointers.)
The Bad:
- Iterators (like pointers) do not provide bounds checking; it is possible to overstep the bounds of a container, resulting in segmentation faults.
- Different containers support different iterators, so it is not always possible to change the underlying container type without making changes to your code.
- Iterators (like pointers) can be invalidated if the underlying container (the container being iterated over) is changed significantly.
See also Dr. Dobb's "STL & Generic Programming: Writing Your Own Iterators"
The Lab
Lab requirements:
- Command line argument #1 (argv[1]) points to the input file name.
- Command line argument #2 (argv[2]} points to the output file name.
- Your linked list class contains a nested iterator class and begin() and end() public member functions that return instantiated iterator objects.
- The iterator class overloads the dereferencing ("*"), pre-incrementing ("++"), and not equal ("!=") operators.
- All classes (including the iterator) have public toString and friend insertion member functions.
- Process the following input commands:
COMMAND / DESCRIPTION | EXAMPLE INPUT (Bold) / OUTPUT |
---|---|
Iterate
Output the contents of the linked list, one element per line, enclosed in brackets. Output "Empty!" if list is empty. LinkedList<string>::Iterator iter; iter = linked_list.begin(); while (iter != linked_list.end()) { cout << endl << " [" << *iter << "]"; ++iter; } |
Insert time. the is Now PrintList Now is the time. Iterate [Now] [is] [the] [time.] Clear OK Iterate Empty! |
Find <item>
Find the first item in the linked list. (Call find(first, last, value)) Output "OK" if successful. Throw an error "Not Found" if <item> is not found. LinkedList |
Insert do. you unless work will Nothing PrintList Nothing will work unless you do. Find Nothing OK Find nothing Not Found |
InsertAfter <item1> <item2>
Insert <item1> element after <item2> element. (Call insert_after(position, value)) Output "OK" if successful. Throw an error "Not Found" if <item2> is not found. LinkedList |
PrintList Now is the time. InsertAfter really is OK PrintList Now is really the time. InsertAfter really Now OK PrintList Now really is really the time. InsertAfter really pig Not Found |
InsertBefore <item1> <item2>
Insert <item1> element before <item2> element. (Call insert(position, value)) Output "OK" if successful. Throw an error "Not Found" if <item2> is not found. LinkedList |
PrintList Now is the time. InsertAfter really is OK PrintList Now is really the time. InsertBefore really is OK PrintList Now really is really the time. InsertBefore really pig Not Found |
Erase <item>
Delete the first occurrance of item in the linked list. (Call erase(position)) Output "OK" if successful. Throw an error "Not Found" if <item> is not found. LinkedList |
Insert Groot. happy happy am I PrintList I am happy happy Groot. Erase happy OK Erase dopey Not Found PrintList I am happy Groot. |
Replace <old> <new>
Replace all <old> elements with <new> element. (Call replace(first, last, old_value, new_value)) Output "OK" if successful. linked_list.replace(linked_list.begin(), linked_list.end(), "too", "very"); |
Insert Groot. happy happy am I Replace happy sad OK PrintList I am sad sad Groot. |
Guidelines:
- No default iterator constructor should be supplied to ensure that every iterator points to a valid container.
- Although the iterator class definition does not necessarily have to be nested, doing so emphasizes that the iterator is part of the container interface.
- The operator* is the dereferencing operator for the container class. It is logical that dereferencing an iterator object returns a reference (&) to the container element to which it points.
- The iterator maintains a pointer to a Node in the parent's container and initialized during the iterator construction.
- An iterator with a NULL pointer is used for end-of-list or an empty list.
- Most iterators have undefined behaviour for invalid references. Throwing an exception to such cases might be a good enhancement.
Steps to Success:
- Step 1 - Begin with a L03: Linked List Lab.
- Verify the following linked list commands work correctly:
- Clear
- Insert
- PrintList
- Verify the following linked list commands work correctly:
- Step 2 - Add nested Iterator class to your linked list class.
(Refer to iterator.cpp)
- Add a nested iterator class as a public member of the linked list class. Create the appropriate constructors / destructor.
- Add begin() and end() functions to the linked list class that return corresponding instantiated iterators.
- Overload the iterator dereferencing ("*") operator to return a reference to a linked list element.
- Overload the iterator pre-increment ("++") operator to move the iterator to the next linked list element.
- Overload the iterator not equal ("!=") operator to compare two iterators and return a bool result.
- Step 3 - Implement commands and corresponding linked list iterator functions to
find, insert, insert_after, erase, and replace items in the linked list.
/** Return iterator pointing found value in linked list */ Iterator find(Iterator first, Iterator last, const T& value) { ... }
/** Return iterator pointing to inserted value in linked list */ Iterator insert(Iterator position, const T& value) { ... }
/** Return iterator pointing to inserted value in linked list */ Iterator insert_after(Iterator position, const T& value) { ... }
/** Return iterator pointing to next item after deleted node linked list */ Iterator erase(Iterator position) { ... }
/** Replace all old_value(s) with new_value */ void replace(Iterator first, Iterator last, const T& old_value, const T& new_value) { ... }
Helps and Hints
Note: Code examples come from readily available, public websites (ie., stackoverflow.com, cplusplus.com, etc.)
Reading and Writing from a File.(collapse)
- Use extraction operator (">>") to read from the input stream. For example:
string line; ifstream in(argv[1]); while (in >> line) { list.push_front(line); }
Understanding Valgrind Output.(collapse)
- You can find helpful hints and explanations of Valgrind output here.
Iterator Grading Criteria
Instructions for submitting your lab:
- Zip all your lab source files (.cpp, .hpp, .h) into one folder. DO NOT INCLUDE YOUR WHOLE PROJECT!
- Submit your zipped lab source using the "Labs" tab on the class website.
- Your submitted lab source will be compiled and executed using a lab g++ compiler.
- Both input and output files will be specified by command line arguments.
- An auto-grader will compare your output files with expected results and you will shortly receive your score by email.
- The auto-grade process may abort on the first error. If so, correct the error and try again.
- You may re-submit your zipped source as many times as you like.
You may use the following test input and resulting output files in testing your Iterator lab.
Input File | Output File | |
Test #1 | lab04_in_01.txt | lab04_out_01.txt |
Test #2 | lab04_in_02.txt | lab04_out_02.txt |
Test #3 | lab04_in_03.txt | lab04_out_03.txt |
Test #4 | lab04_in_04.txt | lab04_out_04.txt |
Test #5 | lab04_in_05.txt | lab04_out_05.txt |
The auto-grader will test and grade the following sections:
Fail |
Pass |
||
---|---|---|---|
The lab source will be peer reviewed using the following rubric:
No |
Partial |
Yes
|
||
---|---|---|---|---|
Overall Rating |
Easy to follow, readable, organized, and
well commented. |
|||
***NOTE: Any attempt to circumvent any lab requirement (ie, hard coding output, using loops instead of iterators where required, modifying a class interface, etc.) will be reported by peer reviewers and result in a zero score for the lab. |