CS 70

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!)

What's on the heap?

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";
  • Dog speaking

    Wow, that totally looks like we're accessing an array.

  • LHS Cow speaking

    Yes. That's the cool thing. We can make our own types and have them seem just like the built-in ones.

  • Cat speaking

    Is it still the case that a[b] = *(a+b)?

  • LHS Cow speaking

    Not when we make our own. We get to decide how it works.

  • Duck speaking

    Can we define our own operator*, too?

  • LHS Cow speaking

    We could, but we'll leave that idea for another time.

  • Hedgehog speaking

    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.

  • Cat speaking

    Is this really a good idea? It feels like we're exposing internal implementation details.

  • LHS Cow speaking

    Well, on the positive side, we can now use all kinds of algorithms that work on arrays.

  • RHS Cow speaking

    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);

Look over the “behaves more like a built-in array” version in the online compiler (link above). There's a lot to take in, but is there something cool or interesting that you see in this version?

(When logged in, completion status appears here.)