Skip to content

13.4. A Copy-Control Example

Although copy control is most often needed for classes that allocate resources, resource management is not the only reason why a class might need to define these members. Some classes have bookkeeping or other actions that the copy-control members must perform.

As an example of a class that needs copy control in order to do some bookkeeping, we’ll sketch out two classes that might be used in a mail-handling application. These classes, Message and Folder, represent, respectively, email (or other kinds of) messages, and directories in which a message might appear. Each Message can appear in multiple Folders. However, there will be only one copy of the contents of any given Message. That way, if the contents of a Message are changed, those changes will appear when we view that Message from any of its Folders.

To keep track of which Messages are in which Folders, each Message will store a set of pointers to the Folders in which it appears, and each Folder will contain a set of pointers to its Messages. Figure 13.1 illustrates this design.

Image

Figure 13.1. Message and Folder Class Design

Our Message class will provide save and remove operations to add or remove a Message from a specified Folder. To create a new Message, we will specify the contents of the message but no Folder. To put a Message in a particular Folder, we must call save.

When we copy a Message, the copy and the original will be distinct Messages, but both Messages should appear in the same set of Folders. Thus, copying a Message will copy the contents and the set of Folder pointers. It must also add a pointer to the newly created Message to each of those Folders.

When we destroy a Message, that Message no longer exists. Therefore, destroying a Message must remove pointers to that Message from the Folders that had contained that Message.

When we assign one Message to another, we’ll replace the contents of the left-hand Message with those in the right-hand side. We must also update the set of Folders, removing the left-hand Message from its previous Folders and adding that Message to the Folders in which the right-hand Message appears.

Looking at this list of operations, we can see that both the destructor and the copy-assignment operator have to remove this Message from the Folders that point to it. Similarly, both the copy constructor and the copy-assignment operator add a Message to a given list of Folders. We’ll define a pair of private utility functions to do these tasks.

TIP

Best Practices

The copy-assignment operator often does the same work as is needed in the copy constructor and destructor. In such cases, the common work should be put in private utility functions.

The Folder class will need analogous copy control members to add or remove itself from the Messages it stores.

We’ll leave the design and implementation of the Folder class as an exercise. However, we’ll assume that the Folder class has members named addMsg and remMsg that do whatever work is need to add or remove this Message, respectively, from the set of messages in the given Folder.

The Message Class

Given this design, we can write our Message class as follows:

c++
class Message {
    friend class Folder;
public:
    // folders is implicitly initialized to the empty set
    explicit Message(const std::string &str = ""):
        contents(str) { }
    // copy control to manage pointers to this Message
    Message(const Message&);            // copy constructor
    Message& operator=(const Message&); // copy assignment
    ~Message();                         // destructor
    // add/remove this Message from the specified Folder's set of messages
    void save(Folder&);
    void remove(Folder&);
private:
    std::string contents;      // actual message text
    std::set<Folder*> folders; // Folders that have this Message
    // utility functions used by copy constructor, assignment, and destructor
    // add this Message to the Folders that point to the parameter
    void add_to_Folders(const Message&);
    // remove this Message from every Folder in folders
    void remove_from_Folders();
};

The class defines two data members: contents, to store the message text, and folders, to store pointers to the Folders in which this Message appears. The constructor that takes a string copies the given string into contents and (implicitly) initializes folders to the empty set. Because this constructor has a default argument, it is also the Message default constructor (§ 7.5.1, p. 290).

The save and remove Members

Aside from copy control, the Message class has only two public members: save, which puts the Message in the given Folder, and remove, which takes it out:

c++
void Message::save(Folder &f)
{
    folders.insert(&f); // add the given Folder to our list of Folders
    f.addMsg(this);     // add this Message to f's set of Messages
}

void Message::remove(Folder &f)
{
    folders.erase(&f); // take the given Folder out of our list of Folders
    f.remMsg(this);    // remove this Message to f's set of Messages
}

To save (or remove) a Message requires updating the folders member of the Message. When we save a Message, we store a pointer to the given Folder; when we remove a Message, we remove that pointer.

These operations must also update the given Folder. Updating a Folder is a job that the Folder class controls through its addMsg and remMsg members, which will add or remove a pointer to a given Message, respectively.

Copy Control for the Message Class

When we copy a Message, the copy should appear in the same Folders as the original Message. As a result, we must traverse the set of Folder pointers adding a pointer to the new Message to each Folder that points to the original Message. Both the copy constructor and the copy-assignment operator will need to do this work, so we’ll define a function to do this common processing:

c++
// add this Message to Folders that point to m
void Message::add_to_Folders(const Message &m)
{
    for (auto f : m.folders) // for each Folder that holds m
        f->addMsg(this); // add a pointer to this Message to that Folder
}

Here we call addMsg on each Folder in m.folders. The addMsg function will add a pointer to this Message to that Folder.

The Message copy constructor copies the data members of the given object:

c++
Message::Message(const Message &m):
    contents(m.contents), folders(m.folders)
{
    add_to_Folders(m); // add this Message to the Folders that point to m
}

and calls add_to_Folders to add a pointer to the newly created Message to each Folder that contains the original Message.

The Message Destructor

When a Message is destroyed, we must remove this Message from the Folders that point to it. This work is shared with the copy-assignment operator, so we’ll define a common function to do it:

c++
// remove this Message from the corresponding Folders
void Message::remove_from_Folders()
{
    for (auto f : folders) // for each pointer in folders
        f->remMsg(this);   // remove this Message from that Folder
}

The implementation of the remove_from_Folders function is similar to that of add_to_Folders, except that it uses remMsg to remove the current Message.

Given the remove_from_Folders function, writing the destructor is trivial:

c++
Message::~Message()
{
    remove_from_Folders();
}

The call to remove_from_Folders ensures that no Folder has a pointer to the Message we are destroying. The compiler automatically invokes the string destructor to free contents and the set destructor to clean up the memory used by those members.

Message Copy-Assignment Operator

In common with most assignment operators, our Folder copy-assignment operator must do the work of the copy constructor and the destructor. As usual, it is crucial that we structure our code to execute correctly even if the left- and right-hand operands happen to be the same object.

In this case, we protect against self-assignment by removing pointers to this Message from the folders of the left-hand operand before inserting pointers in the folders in the right-hand operand:

c++
Message& Message::operator=(const Message &rhs)
{
    // handle self-assignment by removing pointers before inserting them
    remove_from_Folders();   // update existing Folders
    contents = rhs.contents; // copy message contents from rhs
    folders = rhs.folders;   // copy Folder pointers from rhs
    add_to_Folders(rhs);     // add this Message to those Folders
    return *this;
}

If the left- and right-hand operands are the same object, then they have the same address. Had we called remove_from_folders after calling add_to_folders, we would have removed this Message from all of its corresponding Folders.

A swap Function for Message

The library defines versions of swap for both string and set9.2.5, p. 339). As a result, our Message class will benefit from defining its own version of swap. By defining a Message-specific version of swap, we can avoid extraneous copies of the contents and folders members.

However, our swap function must also manage the Folder pointers that point to the swapped Messages. After a call such as swap(m1, m2), the Folders that had pointed to m1 must now point to m2, and vice versa.

We’ll manage the Folder pointers by making two passes through each of the folders members. The first pass will remove the Messages from their respective Folders. We’ll next call swap to swap the data members. We’ll make the second pass through folders this time adding pointers to the swapped Messages:

c++
void swap(Message &lhs, Message &rhs)
{
    using std::swap; // not strictly needed in this case, but good habit
    // remove pointers to each Message from their (original) respective Folders
    for (auto f: lhs.folders)
        f->remMsg(&lhs);
    for (auto f: rhs.folders)
        f->remMsg(&rhs);
    // swap the contents and Folder pointer sets
    swap(lhs.folders, rhs.folders);     // uses swap(set&, set&)
    swap(lhs.contents, rhs.contents);   // swap(string&, string&)
    // add pointers to each Message to their (new) respective Folders
    for (auto f: lhs.folders)
        f->addMsg(&lhs);
    for (auto f: rhs.folders)
        f->addMsg(&rhs);
}

INFO

Exercises Section 13.4

Exercise 13.33: Why is the parameter to the save and remove members of Message a Folder&? Why didn’t we define that parameter as Folder? Or const Folder&?

Exercise 13.34: Write the Message class as described in this section.

Exercise 13.35: What would happen if Message used the synthesized versions of the copy-control members?

Exercise 13.36: Design and implement the corresponding Folder class. That class should hold a set that points to the Messages in that Folder.

Exercise 13.37: Add members to the Message class to insert or remove a given Folder* into folders. These members are analogous to Folder’s addMsg and remMsg operations.

Exercise 13.38: We did not use copy and swap to define the Message assignment operator. Why do you suppose this is so?