CS 70

Before You Start

We've come quite a long way. So far, we've learned:

  • How to make classes in C++.
  • How to define our own versions of C++ operators for our class to make our classes look and behave like C++'s built-in
    • Examples: =, <<, and even [index].
  • How to allocate memory dynamically on the heap.
    • Either single objects or arrays.
    • The Maze assignment showed us that
      • Successively doubling is a good way to handle growing arrays.
      • What things we need to be careful about when growing objects.
  • Two ways to loop over an array:
    • Using an integer index.
    • Start/past-the-end pointers.

Looking Back at Our CheckList Class

At the end of our last lesson, we'd made a CheckList class with this header:

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

    // 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 provide two overloaded begin/end functions one for writable checklists
    // and one for read-only ones.

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

#endif  // CHECKLIST_HPP_INCLUDED

You can explore the rest of the code and run it on OnlineGDB.

Or if you like, you can view it here:

Other files

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::operator[](size_t index) {
    return steps_[index];
}

const std::string& CheckList::operator[](size_t index) const {
    return steps_[index];
}

std::string* CheckList::begin() {
    return steps_;
}

std::string* CheckList::end() {
    return steps_ + numSteps_;
}

const std::string* CheckList::begin() const {
    return steps_;
}

const std::string* CheckList::end() const {
    return steps_ + numSteps_;
}

void CheckList::printToStream(std::ostream& out) const {
    out << task_ << ":" << std::endl;
    for (std::string* p = steps_; p != steps_ + numSteps_; ++p) {
        out << "  - " << *p << std::endl;
    }
}

CheckList operator+(const CheckList& lhs, const CheckList& rhs) {
    CheckList result{lhs.task() + " & " + rhs.task(), lhs.size() + rhs.size()};
    std::copy(lhs.begin(), lhs.end(), result.begin());
    std::copy(rhs.begin(), rhs.end(), result.begin() + lhs.size());

    return result;
}

std::ostream& operator<<(std::ostream& out, const CheckList& list) {
    list.printToStream(out);
    return out;
}

checklist-test.cpp:

#include "checklist.hpp"
#include <algorithm>
#include <iostream>
#include <string>

int main() {
    CheckList makeDinner{"Sausage and Mash", 7};
    makeDinner[0] = "Peel potatoes";
    makeDinner[1] = "Cook potatoes";
    makeDinner[2] = "Cook vegetables";
    makeDinner[3] = "Grill Sausages";
    makeDinner[4] = "Make Gravy";
    makeDinner[5] = "Mash Potatoes";
    makeDinner[6] = "Serve";

    // Now we use the output operator, rather than printToSteam
    std::cout << makeDinner << std::endl;

    CheckList makeBevs{"Iced Tea", 3};
    makeBevs[0] = "Brew Tea";
    makeBevs[1] = "Chill";
    makeBevs[2] = "Serve";
    std::cout << makeBevs << std::endl;

    // Here we use a plus operator that combines two checklists into
    // one.
    CheckList makeAll = makeDinner + makeBevs;

    // Using the begin/past-the-end pointers, we can call built-in C++
    // algorithms that work on arrays.
    std::sort(makeAll.begin(), makeAll.end());

    // We can make a for loop that iteraters over all the items in our
    // checklist using begin/past-the-end style, much like we would for
    // a built-in array.  Here we go through all our check-list items
    // and add a question mark.
    for (std::string* p = makeAll.begin(); p != makeAll.end(); ++p) {
        std::string& checkItem = *p;
        checkItem += '?';
    }

    // And, even cooler, if we have .begin() and .end(), we can use a NEW
    // KIND of for loop that works just like the loop above, but hides
    // the details of calling .begin() and .end() and the pointer.
    // Here we go through all our check-list items and add an exclamation
    // mark.
    for (std::string& checkItem : makeAll) {
        checkItem += '!';
    }

    std::cout << makeAll << std::endl;
}

Explain what the begin() and end() member functions do for a CheckList object, and why there are two pairs of them, one pair of begin/end that works on writable CheckLists and one that works on read-only CheckLists.

The begin() and end() member functions return a pointer to the internal array of CheckList items, allowing us to go through them with a standard begin/past-the-end loop.

The begin() and end() member functions are overloaded based on whether the object they're being invoked on is const (i.e., read-only) or not.

In other words, if we have a CheckList object called myList,

  • If myList is const (read-only), and we say myList.begin(), it calls the const version of myList.begin(), which returns a const std::string* (which doesn't allow us to change the checklist items).
  • If myList is non-const (can be written to), myList.begin() will call the non-const version of begin*() that returns a std::string* (which can be used to both read and write a checklist item).
  • Hedgehog speaking

    I know we've done it before, but it still seems weird and confusing to put const at the end of the line, not attached to anything, just sort of dangling there….

  • LHS Cow speaking

    That's how we mark the target object (the object that this points to) as const.

  • RHS Cow speaking

    It is a bit of an odd placement, but there's nowhere else the C++ standard could have put it. A const at the front of the line specifies the return type, and a const inside the parentheses would mark one of the explicit arguments as being const.

(When logged in, completion status appears here.)