Putting It All Together
Let's put together what we've seen in this lesson and the previous one. We'll create a CheckList
class for use in a to-do–list application.
checklist.hpp
#ifndef CHECKLIST_HPP_INCLUDED
#define CHECKLIST_HPP_INCLUDED
#include <string>
#include <iostream>
class CheckList {
public:
CheckList() = delete;
CheckList(std::string task, size_t numSteps);
CheckList(const CheckList& other);
~CheckList();
CheckList& operator=(CheckList rhs); // copy-and-swap idiom
void swap(CheckList& other);
std::string task() const;
size_t size() const;
std::string getStep(size_t index) const;
void setStep(size_t index, std::string newValue);
void printToStream(std::ostream& out);
private:
std::string task_;
size_t numSteps_;
std::string* steps_;
};
#endif // CHECKLIST_HPP_INCLUDED
checklist.cpp
#include "checklist.hpp"
#include <string>
CheckList::CheckList(std::string task, size_t numSteps)
: task_{task}, numSteps_{numSteps}, steps_{new std::string[numSteps_]} {
// Array of strings on heap has all elements default initialized to
// empty strings, which is fine, so nothing (else) to do.
}
CheckList::CheckList(const CheckList& other)
: task_{other.task_},
numSteps_{other.numSteps_},
steps_{new std::string[numSteps_]} {
// Rather than hand-write a for loop, this code uses std::copy to copy
// the array. The arguments are:
// source_start, source_past_end, dest_start
std::copy(other.steps_, other.steps_ + other.numSteps_, steps_);
}
CheckList::~CheckList() {
delete[] steps_;
}
CheckList& CheckList::operator=(CheckList rhs) { // copy-and-swap idiom
swap(rhs);
return *this;
}
void CheckList::swap(CheckList& other) {
std::swap(task_, other.task_);
std::swap(numSteps_, other.numSteps_);
std::swap(steps_, other.steps_);
}
size_t CheckList::size() const {
return numSteps_;
}
std::string CheckList::task() const {
return task_;
}
std::string CheckList::getStep(size_t index) const {
return steps_[index];
}
void CheckList::setStep(size_t index, std::string newValue) {
steps_[index] = newValue;
}
void CheckList::printToStream(std::ostream& out) {
out << task_ << ":" << std::endl;
for (std::string* p = steps_; p != steps_ + numSteps_; ++p) {
out << " - " << *p << std::endl;
}
}
checklist-test.cpp
#include "checklist.hpp"
int main() {
CheckList makeDinner{"Sausage and Mash", 7};
makeDinner.setStep(0, "Peel potatoes");
makeDinner.setStep(1, "Cook potatoes");
makeDinner.setStep(2, "Cook vegetables");
makeDinner.setStep(3, "Grill Sausages");
makeDinner.setStep(4, "Make Gravy");
makeDinner.setStep(5, "Mash Potatoes");
makeDinner.setStep(6, "Serve");
makeDinner.printToStream(std::cout);
}
You can check out this code and run it.
Memory Diagram Practice
Draw out a memory diagram showing what's in memory just after line six of checklist-test.cpp
, where we've run:
makeDinner.setStep(1, "Cook potatoes");
(You don't need to model any of the function or constructor calls in detail, just the stack frame of main
and the contents of the heap. But you can do more if you want!)
Being More Array-Like
Our getStep
and setStep
member functions work, but they feel a bit clumsy.
So let's replace them with a single function that returns a reference to that step in the checklist. We could define that function as
std::string& CheckList::step(size_t index) {
return steps_[index];
and use it with
makeDinner.step(1) = "Cook potatoes";
But instead we'll create an array-indexing operator
std::string& CheckList::operator[](size_t index) {
return steps_[index];
and use it like
makeDinner[1] = "Cook potatoes";
Wow, that totally looks like we're accessing an array.
Yes. That's the cool thing. We can make our own types and have them seem just like the built-in ones.
Is it still the case that
a[b] = *(a+b)
?Not when we make our own. We get to decide how it works.
Can we define our own
operator*
, too?We could, but we'll leave that idea for another time.
Good!
Begin and Past-the-End
If we want the items on our to-do list to feel even more array-like, we could provide begin and past-the-end functionality by returning pointers to the first and last elements in our internal array:
std::string* CheckList::begin() {
return steps_;
}
std::string* CheckList::end() {
return steps_ + numSteps_;
}
These functions give people another way of iterating over our checklist. We can even sort our checklist now with
std::sort(makeDinner.begin(), makeDinner.end());
because std::sort
takes a pointer to the first element of an array and a pointer to the past-the-end position.
Is this really a good idea? It feels like we're exposing internal implementation details.
Well, on the positive side, we can now use all kinds of algorithms that work on arrays.
But, on the other hand, yes, you're right, it does feel a bit sketchy. We'll work on that issue in a future lesson.
Check It Out
You can try this code yourself!
Here's the header class definition (and definitions for CheckList
-specific versions of +
and <<
) from the header:
class CheckList {
public:
CheckList() = delete;
CheckList(std::string task, size_t numSteps);
CheckList(const CheckList& other);
~CheckList();
CheckList& operator=(CheckList rhs); // copy-and-swap idiom
void swap(CheckList& other);
std::string task() const;
size_t size() const;
// We provide two overloaded operator[], one for writable checklists
// and one for read-only ones.
std::string& operator[](size_t index);
const std::string& operator[](size_t index) const;
// We similarly provide two begin/end functions, one writable, one not.
std::string* begin();
std::string* end();
const std::string* begin() const;
const std::string* end() const;
void printToStream(std::ostream& out) const;
private:
std::string task_;
size_t numSteps_;
std::string* steps_;
};
// We'll provide a global function that adds two checklists
CheckList operator+(const CheckList& lhs, const CheckList& rhs);
// We'll provide a global function that prints a checklist.
std::ostream& operator<<(std::ostream& out, const CheckList& list);
(When logged in, completion status appears here.)