CS 70

Putting It All Together

  • LHS Cow speaking

    This is a big exercise that involves a bunch of stuff we just discussed.

  • RHS Cow speaking

    It just goes to show how much code is implicitly running during steps that seem very simple!

  • LHS Cow speaking

    It's important to know what functions are going to implicitly run when so you can, for instance, debug them properly.

Practice

The following two classes represent a rudimentary to-do list system.

The Task class represents a single to-do item and the ToDo class represents a to-do list.

A ToDo object stores an array of three Tasks. By default a Task is named None and marked complete. The Tasks in the array can be filled in as the user adds Tasks to the list.

(We've left off some of the interface comments that we'd normally expect to have to save you some scrolling.)

class Task {
  public:
    /**
     * \brief Creates a completed task with name "None"
     */
    Task();

    Task(const std::string& name);

    /**
     * \brief Creates a task that copies another task
     * \param other  the task to copy
     */
    Task(const Task& other);

    ~Task();

    /**
     * \brief Makes this task a copy of a given task
     * \param rhs  the task to copy
     * \returns a reference to this task
     */
    Task& operator=(const Task& rhs);

    std::string getName() const;

    bool isComplete() const;

  private:
    std::string name_;
    bool complete_;
};
class ToDo {
  public:
    /**
     * \brief Creates an empty to-do list
     */
    ToDo();

    /**
     * \brief Creates a to-do list that copies another to-do list
     * \param other  the ToDo to copy
     */
    ToDo(const ToDo& other);

    ~ToDo();

    /**
     * \brief Makes this ToDo a copy of a given ToDo
     * \param rhs  the ToDo to copy
     * \returns a reference to this ToDo
     */
    ToDo& operator=(const ToDo& rhs);

    void addTask(Task t);

    Task getTask(size_t idx) const;

    size_t getNumTasks() const;

  private:
    /**
     * \brief Makes this ToDo a copy of a given ToDo
     * \param other  the ToDo to copy
     */
    void copyTasks(const ToDo& other);

    static constexpr size_t NUMTASKS_ = 3;
    Task tasks_[NUMTASKS_];
    size_t numTasks_;
};

  • Duck speaking

    What's that static constexpr thing in ToDo?

  • LHS Cow speaking

    Oh, good catch!

A static member is a property of the class, not an individual object. It exists for the entire length of the program and all objects of the class share the same value.

Remember how in Java you wrote public static void main so that the main function could be called before any objects existed? It's basically the same thing, but with a data member.

We'll talk about it a bit more in the homework. For now it's most relevant to know that static members are not stored on the stack (because they exist before any functions have been called!) but the code in the class can refer to static members like any other member variable.

Here are the member-function definitions. They do what you'd expect them to do, but they also print some things out.

Task::Task() :
    name_{"None"},
    complete_{true} {
    cout << "Task default constructor" << endl;
}

Task::Task(const string& name) :
    name_{name},
    complete_{false} {
    cout << "Task parameterized constructor" << endl;
}

Task::Task(const Task& other) :
    name_{other.name_},
    complete_{other.complete_} {
    cout << "Task copy constructor" << endl;
}

Task::~Task() {
    cout << "Task destructor" << endl;
    // Nothing else to do
}

Task& Task::operator=(const Task& rhs) {
    name_ = rhs.name_;
    complete_ = rhs.complete_;
    cout << "Task assignment operator" << endl;

    // Don't worry about this line. It will make sense soon!
    return *this;
}

std::string Task::getName() const {
    return name_;
}

bool Task::isComplete() const {
    return complete_;
}
ToDo::ToDo() :
    numTasks_{0} {
    cout << "ToDo default constructor" << endl;
}

ToDo::ToDo(const ToDo& other) :
    numTasks_{other.numTasks_} {
    cout << "ToDo copy constructor" << endl;
    copyTasks(other);
}

ToDo::~ToDo() {
    cout << "ToDo destructor" << endl;
    // Nothing else do to
}

ToDo& ToDo::operator=(const ToDo& rhs) {
    cout << "ToDo assignment operator" << endl;
    numTasks_ = rhs.numTasks_;
    copyTasks(rhs);

    // Don't worry about this line. It will make sense soon!
    return *this;
}

void ToDo::addTask(Task t) {
    tasks_[numTasks_] = t;
    ++numTasks_;
}

Task ToDo::getTask(size_t idx) const {
    return tasks_[idx];
}

size_t ToDo::getNumTasks() const {
    return numTasks_;
}

void ToDo::copyTasks(const ToDo& other) {
    for (size_t i = 0; i < NUMTASKS_; ++i) {
       tasks_[i] = other.tasks_[i];
    }
}

Now see if you can trace through the following program that uses both classes:

void printIncomplete(ToDo list) {
    // Give
    size_t numTasks = list.getNumTasks();
    for (size_t i = 0; i < numTasks; ++i) {
        Task t = list.getTask(i);
        if (!t.isComplete()) {
        cout << t.getName() << endl;
        }
    }
    // You
}

int main() {
    ToDo myList;
    Task myTask{"Object lifetime example"};
    // Never
    myList.addTask(myTask);
    // Gonna
    printIncomplete(myList);
    return 0;
}  // Up
  • Hedgehog speaking

    I'm scared!

  • LHS Cow speaking

    You can do this. Take your time.

  • RHS Cow speaking

    Go systematically step-by-step so you don't get overwhelmed.

  • LHS Cow speaking

    Draw pictures!

  • Jo speaking

    Have some coffee!!

What is printed between the program start and // Never?

What is printed between that and // Gonna?

What is printed between that and // Give?

What is printed between that and // You?

What is printed between that and // Up?

Want to Play with the Code…?

If you want to really run it, rather than hand-simulate it, you can find the code here:

Bonus—Deeper Dive: An Ugly Truth about Primitive Arrays

We've covered what we want you to take away. This is just some esoteric C++ knowledge if you are interested in that kind of stuff.

  • Rabbit speaking

    Oh, I love it when we head down one of these rabbit holes!

  • Goat speaking

    Meh, I'm skipping it!

In the copy constructor and assignment operator for ToDo, we wanted to copy the tasks_ array. We did it ourselves, one element at a time.

It turns out that if we had used the compiler's synthesized copy constructor instead of writing our own, it would have copied the array and everything would be good. But we wanted to write our own (so it could print a message).

Unfortunately, C++ doesn't allow us to copy a primitive array in a member-initialization list! (Why? Because primitive arrays don't have any concept of a copy constructor, which is what we're trying to use here.)

Thus, when we wrote our own copy constructor, we had to skip member initialization for the tasks_ array and allow it to be default initialized. Afterwards, in the body of the constructor, we used an assignment statement (in a loop) to change the contents of the array.

It seems rather unfair that the compiler's synthesized code can do things we can't, but there are a variety of workarounds. The easiest change we could make to our example code is to modify the declaration of tasks_ to use a very slightly enhanced version of primitive arrays called std::array, which wraps an array inside another class (with its own compiler-synthesized copy constructor!). Then we could copy construct tasks_ using the member-initialization list, making the copyTasks function unnecessary.

(When logged in, completion status appears here.)