Info Assignments Office Hours Hall of Fame Notes
Info Assignments Office Hours Hall of Fame Notes

Linked Lists

Assigned: 2020-10-20
Due Date: 2020-10-27 by 11:59PM U.S. Central Time

If you're having some trouble on this assignment, click here for some pointers!

Assignment Spec

In this assignment, your task is to implement a templated linked list.

Getting Started

Get your copy of the assignment repo here: https://classroom.github.com/a/AE7tSI6Y

For this assignment, we have given you two files:

Background Knowledge

Linked Lists

A linked list is a very standard data structure in computer science. It is designed as an alternative to arrays as it can do some operations more efficiently. For example, in a linked list, you can remove an element from the list without having to copy other data as you would need to do in an array.

The basic implementation of a linked list uses a head pointer which points to the first element of the list, with each element pointing to the next element, and finally the last element points to null. An illustration of this is shown below:

The most basic operation of a linked list is to insert a node at the front of the list. This is done using the following steps:

This process can be repeated several times to insert more nodes in a list.

The process to remove a node from the front of the list is similar:

Again, to empty the list, this can be repeated until the head pointer points to null. This will effectively empty the list and deallocate all of its memory.

To remove a node from later in this list, you simply traverse the list to find the node before the node you wish to delete, and then execute the removal procedure as if the next pointer in the node you have found was the head pointer. Similarly, to insert a node at the end of the list, you need to find the last node in the list and treat that node as the head node, executing the procedure for inserting at the head with the pointer in that node being treated as the head pointer.

The following website is a helpful tool for visualizing the behavior of these abstract data functions applied to a linked list: https://visualgo.net/en/list

C++ Templates

What good would a linked list be if we could only store one type of data in it? But then again, wouldn't we then have to define a ton of linked list node types to accommodate all the possible datatypes? Luckily, there is a better way: templates!

C++ templates are most similar to generics in Java, which you have already used with ArrayList e.g.: ArrayList<Double>, or ArrayList<Integer>. Templates/generics are a way to parameterize the type of a function or class. For instance, say we wanted to implement a function that returns the greater of its two parameters (the max function). We don't care what type the two parameters are; as long as they have some inherent ordering, they are valid candidates for our function. So we abstract the type away to a template type:

template <class T>

Now "T" represents some arbitrary type. From here, we just define the maximum function normally, except we use T as the type specifier:

template <class T>
T Max(T lhs, T rhs) {
    if (lhs > rhs) {
        return lhs;
    } else {
        return rhs;
    }
}

A question that might have crossed your mind is: why are we implementing the list in a header file that is included in another header file? And where is the cpp file? The reason we do this is to work around a C++ template restriction. Behind the scenes, when the compiler reads a section like:

int a = 8;
int b = 5;
int x = Max(a, b);

...it then creates an implementation for Max that accepts ints. If the template declaration for Max was in a cpp file, then the compiler could not read the source, and would have no idea how to make its int version of Max. But as long as Max is implemented in an included header, the compiler will be able to read it, and substitute int for T to make the requisite function.

Be warned: if you were to pass a type in to this function that did not have operator > overloaded, you would then get a compiler error, and would either have to overload operator > or find an other way of getting the max.

C++ Big 5

A large part of C++'s resource management model are the big five functions: the copy constructor, the move constructor, the destructor, the copy assignment operator, and the move assignment operator. The Rule of the Big 5 is that if any one of these are implemented in a C++ class, all of them must be implemented. The idea behind this is that C++ assumes very little when it comes to memory management. A lot of C++ objects manage sub-object memory, like our LinkedList that manages many Nodes' memory. The best C++ can do without further help is create the memory when the LinkedList is initialized, and destroy it when it goes out of scope. However, the larger object can be copied, moved around, and destroyed early, and C++ would have no idea what to do then. This is where the Big 5 come in to save the day. If you write proper Big 5 functions, you can expect your object's memory to be properly freed when the time comes.

1. Copy Constructor

MyObject(const MyObject &source);

The job of a copy constructor is to copy over all of the memory of source into this. The memory should not be shared, and should be brand new, belonging only to the new object.

2. Move Constructor

MyObject(MyObject &&source) noexcept;

Unlike the copy constructor, which keeps source intact, the move constructor will consume source, and move source's memory into this. This is why source is an rvalue reference, which is a reference to an object with no permanent memory address.

3. Destructor

~MyObject();

The destructor frees all of the memory associated with an instance of MyObject. Remember that any memory allocated on the heap (using the new keyword) must be freed once it's done being used, or else you will encounter a memory leak.

4. Copy Assignment Operator

MyObject& operator=(const MyObject &source);

The copy assignment operator does almost the same thing as the copy constructor, but it is called on an object that already exists and whose contents are to be replaced.

5. Move Assignment Operator

MyObject& operator=(MyObject &&source) noexcept;

Again, this does the same as the move constructor, but it returns the object that source's memory was moved into.

Copy v.s. Move

Invoking a copy is something you might be quite familiar with already! Here's an example:

std::string string_to_copy = "Nobody likes a copycat!";
std::string new_string; //default constructor
new_string = string_to_copy; // Copy assignment invoked
// string_to_copy is not attached to new_string because it was a deep copy

Invoking move is a little less intuitive. The easiest way is to do the exact same thing you would do with a copy, but wrap the source in a std::move call:

std::string string_to_move = "Movin' out!";
std::string new_string = std::move(string_to_move); // Move assignment invoked
// string_to_move no longer owns that string; new_string does.

That example is helpful for tests, but feels a bit contrived. Here's a real world example of invoking a move:

std::string get_string_to_move() {
    std::string string_to_move = "Gotta move that gear up!";
    return string_to_move;
    // string_to_move is going out of scope with this return, so it no longer
    // needs a copy of its memory
}

std::string new_string; //default constructor
new_string = get_string_to_move(); // Move assignment invoked.

Grading and Deliverables

Restrictions

You are not allowed to use any STL classes other than vector in the vector constructor. You must handle all of the memory yourself using new and delete.

IMPORTANT: You must not create ANY new public methods. For testing, you should be using the interface we've provided and should not make any getters/setters for head_ of your Linked List or make the head_ public. Doing this would violate encapsulation and is particularly dangerous because this allows any outside user of your Linked List library to arbitrarily modify/break any instance of your Linked List. Hint: you implemented something in this MP that you can use to safely iterate through the Linked List.

Hints

We would like to suggest that you implement the insertion and comparison operators to test before handling the operators that remove nodes from the list, since they have a higher chance of having bugs. Again, work in the smallest testable steps possible. Each function should be very small, and you should be testing them as you go. Finally, remember that as you implement each function, you may want to revisit those earlier tests, since you are adding more functionality as you go that could be relevant to those tests.

Assignment Rubric

This rubric is not a comprehensive checklist. Please make sure to go over the feedback you received on your previous MPs and ask your moderator/post on Campuswire if you have any questions.

Also, be sure to read over the Google C++ Style Guide here -- some parts, especially with respect to naming conventions, are quite different from what you’re used to and you will be marked off if you don’t follow the style guide.

Click here to view

C++

  • Headers have inclusion guards (#ifndef or #pragma once)
  • All method specifications are in the header class, and operational line comments are left in the source file.
  • Code has appropriate namespacing
  • Usage of const and references
    • Instance methods marked as const where appropriate
    • Parameters passed as const reference where appropriate
    • For-each loop variables declared as const reference where appropriate. For template types, it's better to pass them as const references (ex: const T& val) than it is to pass them as copies, because passing built in types as const references is less harmful than passing non-built in types as copies.
  • size_t used in lieu of int where appropriate
  • Avoid using namespace std to avoid naming collisions. Instead, only use specific things, e.g. using std::string
  • Proper memory management
    • Every call to new must be matched by a call to delete. Remember that non-built-in types can be allocated on the stack (without using ‘new’ in C++)
    • Avoid returning a pointer/reference to memory on the stack in a function (this is called a “dangling pointer” memory error). If you need a variable to outlast the scope of which it was created in, you need to allocate this variable on the heap.
    • Initialize built-in types allocated on the stack (ex: int x = 0; rather than int x;) before using them
  • Appropriate usage of structs, classes, and namespaces

Readability and flexibility of code

  • Modularity: each method should perform one distinct task
  • It should be easy to read through each method, follow its control flow, and verify its correctness
  • The code should be flexible/ready for change (no magic numbers, no violations of DRY)

Object decomposition

  • Member variables stored by each class
    • Classes should store their data using the most intuitive data structure
    • No "missing" member variables
    • No member variables which should be local variables
    • No redundancy / storing multiple copies of the same data in different formats
  • Encapsulation
    • Appropriate access modifiers
    • Member variables should generally only modified by member functions in the same class
    • The interface of a class should be intuitive/abstract, and external code should only interact with the class via the interface
      • By intuitive, we mean that it should be easy to understand and use the class, and there shouldn’t be any hidden assumptions about how the class should be used
      • By abstract, we mean that an external client shouldn’t need to worry about the internal details of the class
  • No getters/setters -- you should be using the public interface we've provided to test your code.

Documentation

  • Specifications
    • Specifications are required for all functions which are part of the public interface of a class
    • Specifications should precisely describe the inputs and outputs of a function, and should also describe what the function does (e.g. mutating state of object)
    • Specifications should also be formatted properly
  • Inline comments should not describe things which are obvious from the code, and should describe things which need clarification

Naming

  • Semantics: names should effectively describe the entities they represent; they should be unambiguous and leave no potential for misinterpretation. However, they should not be too verbose.
  • Style: names should follow the Google C++ Style Guide

Layout

  • Spacing should be readable and consistent; your code should look professional
  • Vertical whitespace should be meaningful
  • Vertical whitespace can help create paragraphs
  • Having 2+ empty lines in a row, or empty lines at the beginning or end of files, is usually a waste of space and looks inconsistent
  • Horizontal whitespace should be present where required by the Google Style Guide
  • Lines should all be under 100 characters; no horizontal scrolling should be necessary

Testing

  • You should make sure all classes of inputs and outputs are tested.
  • Boundary/edge cases often cause different/unexpected behavior, and thus, they should be tested
  • Your tests should cover all of the functionality that you’ve implemented. In other words, every line of code should be exercised by some test case, unless the assignment documentation says otherwise
    • You should be testing for correctness. Testing whether your model has an accuracy over a certain threshold does not guarantee that your model is correct.
    • Test every non-trivial public method with all possible classes of input
  • Each individual test case should only serve one coherent purpose. Individual test cases should not have assertions testing unrelated things
  • Your tests, like your code, should be organized and easy to understand. This includes:
    • Easy to verify thoroughness / all possibilities covered
    • Easy to verify the correctness of each test case
    • Clear categories of test cases, where similar tests are grouped together
  • Test case descriptions make the purpose of each test case clear
  • Appropriate usage of SECTION and TEST_CASE to organize your code

Process

  • Commit modularity
    • Code should be checked-in periodically/progressively in logical chunks
    • Unrelated changes should not be bundled in the same commit
    • Do not treat commits as save points
  • Commit messages
    • Should concisely and accurately describe the changes made
    • Should have a consistent style and look professional
    • First word of the message should be a verb, and it should be capitalized
    • Commit message header should be no longer than 50 characters; in general, if you find the need to use “and” to group multiple unrelated descriptions together, you should break your commit message up

Presentation

  • Arrived on time with all necessary materials and ready to go
  • Good selection of topics to focus on
  • Logical order of presentation
  • Appropriate pacing and engagement of the fellow students
  • Speaking loud enough and enunciating clearly

Participation

  • Each student should contribute at least one meaningful comment or question for every other student who presents in his/her code review
  • Students must behave respectfully to moderator and other students

Weightings

Your grades for each section of the rubric will be weighted as follows:

  • C++ (5%)
  • Readability and flexibility of code (10%)
  • Object decomposition (2.5%)
  • Documentation (2.5%)
  • Naming (5%)
  • Layout (5%)
  • Testing (30%)
  • Process (5%)
  • Presentation (5%)
  • Participation (5%)
  • Correctness (25%)