13.4. Пример управления копированием

Несмотря на то что управление копированием обычно необходимо для классов, резервирующих ресурсы, управление ресурсами не единственная причина определения этих функций-членов. У некоторых классов может быть необходимость в учете или других действиях, выполняемых функциями управления копированием.

В качестве примера, нуждающегося в управлении копированием класса для учета, рассмотрим два класса, которые могли бы использоваться в приложении обработки почты. Эти классы, Message и Folder, представляют соответственно сообщение электронной (или другой) почты и каталог, в котором могло бы находиться это сообщение. Каждое сообщение может находиться в нескольких папках. Но может существовать только одна копия содержимого любого сообщения. Таким образом, если содержимое сообщения изменится, эти изменения отображаются при просмотре данного сообщения в любой из папок.

Для отслеживания того, какие сообщения в каких папках находятся, каждый объект класса Message будет хранить набор указателей на объекты класса Folder, в которых они присутствуют, а каждый объект класса Folder будет содержать набор указателей на его объекты класса Message. Эту конструкцию иллюстрирует рис. 13.1.

Рис. 13.1. Проект классов Message и Folder

Класс Message будет предоставлять функции save() и remove() для добавления и удаления сообщений из папки. Для создания нового объекта класса Message следует определить содержимое сообщения, но не папку. Чтобы поместить сообщение в определенную папку, следует вызвать функцию save().

После копирования сообщения копия и оригинал будут разными объектами класса Message, но оба сообщения должны присутствовать в том же самом наборе папок. Таким образом, копирование сообщения скопирует содержимое и набор указателей на папку. Он должен также добавить указатель на недавно созданный объект класса Message к каждому из этих объектов класса Folder.

После удаления сообщения объект класса Message больше не существует. Поэтому его удаление должно удалять указатели на этот объект класса Message из всех объектов класса Folder, которые содержали это сообщение.

Когда один объект класса Message присваивается другому, содержимое (contents) левого сообщения заменяется таковым правого. Следует также модифицировать набор папок, удалив левый объект класса Message из предыдущих объектов класса Folder и добавив в них правый.

Глядя на этот список операций, можно заметить, что и деструктор, и оператор присвоения копии должны удалять заданное сообщение из папок, которые указывают на него. Точно так же и конструктор копий, и оператор присвоения копии добавляют объект класса Message в заданный список объекта класса Folder. Для решения этих задач определим пару закрытых вспомогательных функций.

Оператор присвоения копии зачастую осуществляет ту же работу, которая необходима в конструкторе копий и деструкторе. В таких случаях эти действия обычно помещают в закрытые вспомогательные функции.

Класс Folder будет нуждаться в аналогичных функциях-членах управления копированием для добавления и удаления себя из хранящих их объектов класса Message.

Проектирование и реализацию класса Folder оставим читателю в качестве самостоятельного упражнения, но будем подразумевать, что у него есть функции-члены addMsg() и remMsg(), выполняющие все действия по добавлению и удалению заданного сообщения из набора сообщений указанной папки.

Класс Message

С учетом проекта выше можно написать класс Message следующим образом:

class Message {

 friend class Folder;

public:

 // папки неявно инициализируются пустым набором

 explicit Message(const std::string &str = ""):

  contents(str) { }

 // функции управления копированием, контролирующие указатели на

 // это сообщение

 Message(const Message&);            // конструктор копий

 Message& operator=(const Message&); // присвоение копии

 ~Message();                         // деструктор

 // добавить/удалить это сообщение из набора сообщений папки

 void save(Folder&);

 void remove(Folder&);

private:

 std::string contents;      // фактический текст сообщения

 std::set<Folder*> folders; // папки, содержащие это сообщение

 // вспомогательные функции, используемые конструктором копий,

 // оператором присвоения и деструктором

 // добавить это сообщение в папки, на которые указывает параметр

 void add_to_Folders(const Message&);

 // удалить это сообщение из каждой папки в folders

 void remove_from_Folders();

};

Класс определяет две переменные-члена: contents — для хранения текста сообщения и folders — для хранения указателей на объекты класса Folder, в которых присутствует данное сообщение. Получающий строку конструктор копирует ее в переменную contents и (неявно) инициализирует переменную folders пустым набором. Поскольку у этого конструктора есть аргумент по умолчанию, он также является стандартным конструктором класса Message (см. раздел 7.5.1).

Функции-члены save() и remove()

Кроме функций управления копированием, у класса Message есть только две открытых функции-члена: save(), помещающая сообщение в данную папку, и remove(), извлекающая его:

void Message::save(Folder &f) {

 folders.insert(&f); // добавить данную папку в список папок

 f.addMsg(this);     // добавить данное сообщение в набор сообщений

}

void Message::remove(Folder &f) {

 folders.erase(&f); // удалить данную папку из списка папок

 f.remMsg(this);    // удалить данное сообщение из набора сообщений

}

Чтобы сохранить (или удалить) сообщение, требуется модифицировать член folders класса Message. При сохранении сообщения сохраняется указатель на данный объект класса Folder; при удалении сообщения этот указатель удаляется.

Эти функции должны также модифицировать заданный объект класса Folder. Модификация этого объекта является задачей, контролируемой классом Folder при помощи функций-членов addMsg() и remMsg(), которые добавляют или удаляют указатель на данный объект класса Message соответственно.

Управление копированием класса Message

При копировании сообщения копия должна появляться в тех же папках, что и оригинальное сообщение. В результате необходимо перебрать набор указателей класса Folder, добавляя указатель на новое сообщение в каждую папку, на которую указывал оригинал сообщения. Для этого и конструктор копий, и оператор присвоения копии должны будут выполнять те же действия, поэтому определим функцию для этой общей работы:

// добавить это сообщение в папки, на которые указывает m

void Message::add_to_Folders(const Message &m) {

 for (auto f : m.folders) // для каждой папки, содержащей m,

  f->addMsg(this);        // добавить указатель на это сообщение

                          // в данную папку

}

Здесь происходит вызов функции addMsg() для каждого объекта класса Folder в m.folders. Функция addMsg() добавит указатель на этот объект класса Message в данный объект класса Folder.

Конструктор копий класса Message копирует переменные-члены данного объекта:

Message::Message(const Message &m):

 contents(m.contents), folders(m.folders) {

 add_to_Folders(m); // добавить это сообщение в папки, на которые

                    // указывает m

}

А также вызывает функцию add_to_Folders(), чтобы добавить указатель на недавно созданный объект класса Message каждому объекту класса Folder, который содержит оригинал сообщения.

Деструктор класса Message

При удалении объекта класса Message следует удалить это сообщение из папок, которые указывают на него. Это общее действие с оператором присвоения копии, поэтому определим для этого общую функцию:

// удалить это сообщение из соответствующих папок

void Message::remove_from_Folders() {

 for (auto f : folders) // для каждого указателя в folders

  f->remMsg(this);      // удалить это сообщение из данной папки

}

Реализация функции remove_from_Folders() подобна таковой у функции add_to_Folders(), за исключением того, что она использует функцию remMsg() для удаления текущего сообщения.

При наличии функции remove_from_Folders() написать деструктор несложно:

Message::~Message() {

 remove_from_Folders();

}

Вызов функции remove_from_Folders() гарантирует отсутствие у объектов класса Folder указателей на удаленный объект класса Message. Компилятор автоматически вызывает деструктор класса string для освобождения объекта contents, а деструктор класса set освобождает память, используемую элементами набора.

Оператор присвоения копии класса Message

Как обычно, оператор присвоения и оператор присвоения копии класса Folder должны выполнять действия конструктора копий и деструктора. Как всегда, крайне важно структурировать свой код так, чтобы он выполнялся правильно, даже если операнды слева и справа — тот же объект.

В данном случае защита против присвоения самому себе осуществляется за счет удаления указателей на это сообщение из папок левого операнда прежде, чем вставить указатели в папки правого операнда:

Messages Message::operator=(const Message &rhs) {

 // отработать присвоение себе самому, удаляя указатели прежде вставки

 remove_from_Folders();   // обновить существующие папки

 contents = rhs.contents; // копировать содержимое сообщения из rhs

 folders = rhs.folders;   // копировать указатели Folder из rhs

 add_to_Folders(rhs);     // добавить это сообщение к данным папкам

 return *this;

}

Если левый и правый операнды — тот же объект, то у них тот же адрес. Если вызвать функцию remove_from_Folders() после вызова функции add_to_Folders(), это сообщение будет удалено изо всех соответствующих ему папок.

Функция swap() класса Message

Библиотека определяет версии функции swap() для классов string и set (см. раздел 9.2.5). В результате класс Message извлечет пользу из определения собственной версии функции swap(). При определении специфической для класса Message версии функции swap() можно избежать лишних копирований членов contents и folders.

Но наша функция swap() должна также управлять указателями Folder, которые указывают на обмениваемые сообщения. После такого вызова, как swap(m1, m2), указатели Folder, указывающие на объект m1, должны теперь указать на объект m2, и наоборот.

Для управления указателями Folder осуществляются два прохода по всем элементам folders. Первый проход удалит сообщения из соответствующих папок. Затем вызов функции swap() совершит обмен переменных-членов. Второй проход по элементам folders добавляет указатели на обмениваемые сообщения:

void swap(Message &lhs, Message &rhs) {

 using std::swap; // в данном случае не обязательно, но привычка

                  // хорошая

 // удалить указатели на каждое сообщение из их (оригинальных) папок

 for (auto f: lhs.folders)

  f->remMsg(&lhs);

 for (auto f: rhs.folders)

  f->remMsg(&rhs); // обмен наборов указателей contents и folders

 swap(lhs.folders, rhs.folders);   // использует swap(set&, set&)

 swap(lhs.contents, rhs.contents); // swap(string&, string&)

 // добавляет указатели на каждое сообщение в их (новые) папки

 for (auto f: lhs.folders)

  f->addMsg(&lhs);

 for (auto f: rhs.folders)

  f->addMsg(&rhs);

}

Упражнения раздела 13.4

Упражнение 13.33. Почему параметр функций-членов save() и remove() класса Message имеет тип Folder&? Почему этот параметр не определен как Folder или const Folder&?

Упражнение 13.34. Напишите класс Message, как описано в этом разделе.

Упражнение 13.35. Что случилось бы, используй класс Message синтезируемые версии функций-членов управления копированием?

Упражнение 13.36. Разработайте и реализуйте соответствующий класс Folder. Этот класс должен содержать набор указателей на сообщения в той папке.

Упражнение 13.37. Добавьте в класс Message функции-члены удаления и вставки заданного Folder* в folders. Эти члены аналогичны функциям-членам addMsg() и remMsg() класса Folder.

Упражнение 13.38. Для определения оператора присвоения класса Message не использовалась технология копирования и обмена. Почему, по вашему?