C++ Primer 5th Edition, Part 3: Smart Pointers Make It Harder To Teach How To Write Copy Constructors
Last week, I said I'd give a concrete example of how a new C++11 feature changes teaching strategy. The first such example that comes to mind is teaching how to write copy constructors. C++11 makes copy constructors harder to teach because it makes them unnecessary in many contexts in which earlier C++ programmers would have written them. As a result, it is not easy to come up with a genuinely useful example of a copy constructor that is suitable for an introductory book.
On the surface, this claim seems absurd: How can copy constructors not be useful? The problem is not that they're not useful — it's that many of the straightforward ways of using constructors already appear in library classes, so that most simple examples of copy constructors wind up imitating something that's already in the library.
I pointed out last week that early C++ programmers had to write a copy constructor for almost every nontrivial class. The reason was that C++ copied the bits that constituted a class object's memory, rather than following the abstractions that the object's data members introduced. When C++ was changed so that copying a class object copied each of its data members according to those members' own definitions, copy constructors became much less necessary. Nevertheless, there were two important kinds of applications in which copy constructors were essential.
The first such application was data structures that use dynamic memory, such as strings or vectors. Such classes typically follow a form that is both easy to teach and important to learn:
- The class has one or more constructors that set forth the various ways of creating objects of that class. Typically there will be a variable-length data structure that is allocated dynamically, and the class object itself contains a pointer to the dynamic part. It is each constructor's job to initialize that pointer appropriately.
- The copy constructor (and, similarly, the copy-assignment operator) allocates a copy of the dynamic data associated with its source, and makes the object's pointer point to that copy.
- The destructor frees the memory associated with the copy.
C++98 made it harder to use this kind of example for teaching, because it introduced the
vector library classes. Both of these classes (or templates) encapsulate the typical form for a container, and they do it well enough that it is hard to justify a programming example that does nothing more than what the library already does.
However, in both C++98 and C++03, there was still a nice example of using a copy constructor to do something that the library didn't do, namely manage reference counts. The idea is to build a class that contains a pointer to dynamically allocated data and a pointer to an integer that counts how many objects point to the corresponding dynamic data. Then
- Each constructor other than the copy constructor allocates a counter and sets it to 1.
- The copy constructor increments the counter.
- The copy-assignment operator increments the counter associated with the right-hand side, then decrements the counter associated with the left-hand side. If doing so brings the counter to zero, it must deallocate the left-hand side's memory.
- The destructor decrements the counter and deallocates the object's memory if and only if the counter reaches zero.
This programming technique was a mainstay of C++ pedagogy at least as far back as 1990 — until C++11 made the technique almost irrelevant by introducing the
shared_ptr class template. As with container classes,
shared_ptr does pretty much exactly what one wants in order to manage reference counts. As a result, a C++11 class that uses copy constructors in a nontrivial way must make them do more than just copy data or manipulate reference counts, because containers and
shared_ptr do those things so well already.
Let's approach the problem from first principles. We want a class that does more than just copying. Moreover, it's not good enough merely to copy some but not all of the class' data, because it's easy to handle that with a combination of
shared_ptr and containers. So we need a class in which copying isn't just copying. Moreover, it should be possible to imagine how such a class might be used for practical purposes beyond the classroom.
I'll let you think until next week about what such a class might do.