Putting It All Together
This is a big exercise that involves a bunch of stuff we just discussed.
It just goes to show how much code is implicitly running during steps that seem very simple!
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 Task
s. By default a Task
is named None
and marked complete. The Task
s in the array can be filled in as the user adds Task
s 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_;
};
What's that
static constexpr
thing inToDo
?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
I'm scared!
You can do this. Take your time.
Go systematically step-by-step so you don't get overwhelmed.
Draw pictures!
Have some coffee!!
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.
Oh, I love it when we head down one of these rabbit holes!
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.)