ICS 45C Spring 2022
Notes and Examples: Well-Behaved Classes
Includes a code example with the moniker WellBehavedClasses
Background
As we've talked about previously, C++ classes provide a way to define new data types. There is no deep distinction between user-defined types and built-in types. We can build our own types to have all of the same flexibility and expressiveness that the various built-in types have. Additionally, classes do not automatically introduce extra cost into our program — in terms of memory or time — unless we do things with them that introduce that cost. They do, however, provide a design win: the ability to encapsulate a set of data along with a set of operations that can safely manipulate that data. It's often the case in programming that we have to trade away some performance in order to achieve design clarity; C++ classes (if we use them carefully) allow us to have both.
However, in order to use C++ classes effectively, it becomes necessary to do a few things that will seem foreign if you've seen classes in most of the other programming languages that offer them. As usual, C++ gives us more knobs that we can turn to control efficiency and design flexibility, at the cost of additional complexity. And, in some cases, turning those knobs appropriately is paramount not only for achieving performance, but also for achieving correctness. So we don't want to get much farther down the path of learning C++ without being sure we understand how to write classes that are what I call "well-behaved."
What is a "well-behaved" class?
This example explores how to build what I call a "well-behaved" class. The term "well-behaved" is a little bit loose, but here are some things that objects of a well-behaved class do.
The std::string class in the C++ standard library has these properties, so we can expect it to be as clean and easy-to-use as an int — albeit potentially slower to work with, since string operations like concatenation obviously take more time than, say, integer addition. We should want the same for our classes.
Our ArrayList class
In this example, we explore an ArrayList class, which very roughly parallels the behavior of a class built into the C++ standard library (which we'll explore in detail relatively soon) called std::vector. Our ArrayList is an array-based list. It stores a sequence of elements, indexed consecutively starting at zero, providing constant-time access to any element given its index. Unlike an array, though, our ArrayList provides a few conveniences.
Being well-behaved, we expect to be able to do many things with our ArrayList once it's built, without having to worry too much about confusing, error-prone details. We should expect to be able to:
These are the things we want to achieve. In order to get there, though, we'll need to learn some features of C++ that we haven't seen yet.
"The Big Three"
Classes whose objects manage resources that live outside of themselves — such as dynamically-allocated memory, open files, open connections to networks, etc. — generally require three new kinds of functions to be implemented.
These three functions, together, are sometimes called The Big Three, because the need to write one of them usually implies the need to write all three of them, and because they play a vital role in allowing many kinds of objects to manage resources properly.
Not all classes require the Big Three, because defaults are generated if you don't write them. Any class whose member variables are all of well-behaved classes will generally work just fine, because the defaults are sensible in that case:
Most of the built-in types in C++ can be thought of as well-behaved. Pointers are worth thinking about carefully, though, because it's important to understand that destroying a pointer doesn't destroy what the pointer points to, nor does copying a pointer copy what it points to. For this reason, our ArrayList class requires the Big Three, because the dynamically-allocated arrays require manual management — we'll manually create them with the new operator and destroy them with delete[ ]. Placing that manual memory management (mostly) into the Big Three means that code that uses our ArrayList class can remain blissfully unaware of these details, while still achieving the kind of performance it could achieve by managing the arrays itself. That's a big win indeed.
A couple of additional notes
I've introduced a couple of additional twists here that we didn't cover in lecture; these are things we'll see again later, but they fit nicely into this example, so I've extended it to include them.
Note, also, that the Big Three became the Big Five in the C++11 standard, because of the inclusion of a new feature called moving. I left that out of this example for the sake of simplicity, but moving does offer a significant performance optimization for some kinds of objects. We may see the tools for this — move constructors and move assignment operators — later in this course, but we'll wait on them for now; all things in time.
The code
Writing well-behaved classes requires some care, and the accompanying code for this example tries to take as much of that care as possible. Be sure you read through both the code and the associated comments; we'll be relying on your understanding of these details as we go forward from here.
The official moniker for this code example is WellBehavedClasses, so your best bet is to do this:
Alternatively, you can click the link to the tarball below: