Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Упражнение 13.7. Что произойдет при присвоении одного объекта класса StrBlob другому?). У нее нет ни параметров, ни возвращаемого значения: class Foo { public: |
// осуществляемого автоматически
Sales_data() { }
// другие члены как прежде
};
Переменные-члены автоматически удаляются после выполнения (пустого) тела деструктора.
В частности, деструктор класса string будет выполнен для освобождения памяти,
используемой переменной-членом bookNo.
Важно понять, что само тело деструктора не удаляет переменные-члены непосредственно.
Они удаляются в ходе неявной фазы удаления, которая следует за телом деструктора. Тело деструктора выполняется в дополнение к удалению членов, осуществляемому в ходе удаления объекта.Упражнения раздела 13.1.3
Упражнение 13.9. Что такое деструктор? Что делает синтезируемый деструктор? Когда деструктор синтезируется?
Упражнение 13.10. Что произойдет при удалении объекта класса StrBlob? А класса StrBlobPtr?
Упражнение 13.11. Добавьте деструктор в класс HasPtr из предыдущих упражнений.
Упражнение 13.12. Сколько вызовов деструктора происходит в следующем фрагменте кода? bool fcn(const Sales_data *trans, Sales_data accum) {
Sales_data item1(*trans), item2(accum); return item1.isbn() != item2.isbn();
}
Упражнение 13.13. Наилучший способ изучения функций-членов управления копированием и конструкторов — это определить простой класс с этими функциями-членами, каждая из которых выводит свое имя: struct X {
X() {std::cout << "X()" << std::endl;}
X(const X&) {std::cout << "X(const X&)" << std::endl;}
};
Добавьте в структуру X оператор присвоения копии и деструктор, а затем напишите программу, использующую объекты класса X различными способами: передайте их как ссылочный и не ссылочный параметры; динамически зарезервируйте их; поместите в
Page 629/1103
13.1.4. Правило три/пять
Как уже упоминалось, существуют три базовых функции, контролирующих копирование объектов класса: конструктор копий, оператор присвоения копии и деструктор. Кроме того, как будет продемонстрировано в разделе 13.6, по новому стандарту класс может также определить конструктор перемещения и оператор присваивания при перемещении.
Определять все эти функции не обязательно: вполне можно определить один или два из них,
не определяя все. Эти функции можно считать модулями. Если нужен один, не обязательно определять их все. Классы, нуждающиеся в деструкторах, нуждаются в копировании и присвоении
Вот эмпирическое правило, используемое при принятии решения о необходимости определения в классе собственных версий функций-членов управления копированием:
сначала следует решить, нужен ли классу деструктор. Зачастую потребность в деструкторе более очевидна, чем потребность в операторе присвоения или конструкторе копий. Если класс нуждается в деструкторе, он почти наверняка нуждается также в конструкторе копий и операторе присвоения копии.
Используемый в упражнениях класс HasPtr отлично подойдет для примера (см. раздел
13.1.1). Этот класс резервирует динамическую память в конструкторе. Синтезируемый деструктор не будет удалять указатель-член. Поэтому данный класс должен определить деструктор для освобождения памяти, зарезервированной конструктором.
Хоть это и не очевидно, но согласно эмпирическому правилу класс HasPtr нуждается также в конструкторе копий и операторе присвоения копии.
Давайте посмотрим, что было бы, если бы у класса HasPtr был деструктор и синтезируемые версии конструктора копий и оператора присвоения копии: class HasPtr { public:
HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { }
HasPtr() { delete ps; }
// ошибка: HasPtr нуждается в конструкторе копий и операторе
// присвоения копии
//
Page 630/1103
};
В этой версии класса зарезервированная в конструкторе память будет освобождена при удалении объекта класса HasPtr. К сожалению, здесь есть серьезная ошибка! Данная версия класса использует синтезируемые версии операторов копирования и присвоения. Эти функции копируют указатели-члены, а значит, несколько объектов класса HasPtr смогут указывать на ту же область памяти:
HasPtr f(HasPtr hp) //
HasPtr передан по значению, поэтому он
// копируется
{
HasPtr ret = hp; // копирует данный HasPtr
// обработка ret return ret; // ret и hp удаляются
}
Когда функция f() завершает работу, объекты hp и ret удаляются и деструктор класса HasPtr выполняется для каждого из них. Этот деструктор удалит указатель-член и в объекте ret, и в объекте hp. Но эти объекты содержат одинаковое значение указателя. Код удалит тот же указатель дважды, что является серьезной ошибкой (см. раздел 12.1.2) с непредсказуемыми результатами.
Кроме того, вызывающая сторона функции f() может все еще использовать переданный ей объект:
HasPtr p("some values"); f(p); // по завершении f() память, на которую указывает p.ps,
// освобождается
HasPtr q(p); // теперь и p, и q указывают на недопустимую память!
Память, на которую указывает указатель p (и q), больше недопустима. Она была возвращена операционной системе, когда был удален объект hp (или ret)!
Page 631/1103
// конструктор
NoCopy(const NoCopy&) = delete; // без копирования
NoCopy &operator=(const NoCopy&) = delete; // без присвоения
NoCopy() = default; // используйте синтезируемый деструктор
// другие члены
};
Часть = delete указывает компилятору (и читателям кода), что эти функции-члены не определяются преднамеренно .
В отличие от части = default, часть = delete должна присутствовать в первом объявлении удаленной функции. Это различие согласуется со смыслом данных объявлений. Часть =
default влияет только на то, какой код создает компилятор; следовательно, она необходима,
только пока компилятор не создаст код. С другой стороны, компилятор должен знать, что функция удалена, и запретить ее использование другими функциями.
Также в отличие от части = default, часть = delete можно применить для любой функции (=
default применима только к стандартному конструктору или функции-члену управления копированием, которую компилятор может синтезировать). Хотя изначально удаленные функции предназначались для подавления функций-членов управления копированием, они иногда применимы также для воздействия на процесс подбора функции. Деструктор не должен быть удаленной функцией-членом
Следует заметить, что удалять деструктор нельзя. Если его удалить, то не будет никакого способа освободить объект этого типа. Компилятор не позволит определять переменные или создавать временные объекты типа, у которого удален деструктор. Кроме того, нельзя определять переменные или временные объекты класса, обладающего членом, у типа которого удален деструктор. Если у переменной-члена класса удален деструктор, то она не может быть освобождена. Если не может быть удалена переменная-член, не может быть удален и весь объект в целом.
Хотя определить переменные или переменные-члены таких типов нельзя, вполне можно динамически резервировать объекты с удаленным деструктором. Однако впоследствии их нельзя будет освободить: struct NoDtor {
NoDtor() = default; //
Page 634/1103
// конструктор
NoDtor() = delete; // нельзя удалять объекты типа NoDtor
};
NoDtor nd; // ошибка: у NoDtor удаленный деструктор
NoDtor *p = new NoDtor(); // ok: но нельзя удалить p delete p; // ошибка: у NoDtor удаленный деструктор
Невозможно определить объект или удалить указатель на динамически созданный объект типа с удаленным деструктором. Функции-члены управления копированием могут быть синтезированы как удаленные
Как уже упоминалось, если не определены функции-члены управления копированием,
компилятор определит их сам. Аналогично, если класс не определяет конструктор,
компилятор синтезирует стандартный конструктор для этого класса сам (см. раздел 7.1.4).
Для некоторых классов компилятор определяет эти синтезируемые функции-члены как удаленные.
• Синтезируемый деструктор определяется как удаленный, если у класса есть переменная-член, собственный деструктор которой удален или недоступен (например,
private).
• Синтезируемый конструктор копий определяется как удаленный, если у класса есть переменная-член, собственный деструктор которой удален или недоступен. Он также будет удаленным, если у класса есть переменная-член с удаленным или недоступным деструктором.
• Синтезируемый оператор присвоения копии определяется как удаленный, если у класса есть переменная-член с удаленным или недоступным оператором присвоения копии, либо если у класса есть константный или ссылочный член.
• Синтезируемый стандартный конструктор определяется как удаленный, если у класса есть переменная-член с удаленным или недоступным деструктором; или имеется ссылочный член без внутриклассового инициализатора (см. раздел 2.6.1); или есть константная переменная-член, тип которой не определяет стандартный конструктор явно и не имеет внутриклассового инициализатора.
Короче говоря, эти правила означают, что если у класса есть переменная-член, которая не может быть стандартно создана, скопирована, присвоена или удалена, то соответствующая функция-член класса будет удаленной функцией.
Как ни удивительно, но переменная-член, класс которой имеет удаленный или недоступный
Page 635/1103
которые невозможно удалить.
Однако в том, что компилятор не будет синтезировать стандартный конструктор для класса со ссылочным или с константным членом, который не может быть создан стандартно, ничего удивительного нет. Нет ничего удивительного и в том, что класс с константным членом не может использовать синтезируемый оператор присвоения копии: в конце концов, этот оператор пытается присвоить значения всем членам классов. Однако присвоить новое значение константному объекту невозможно.
Хотя вполне возможно присвоить новое значение ссылке, это изменит значение объекта, на который она ссылается. Если бы оператор присвоения копии синтезировался для таких классов, то левый операнд продолжил бы ссылаться на тот же объект, что и перед присвоением. Он не ссылался бы на тот же объект, что и правый операнд. Поскольку это поведение вряд ли будет желательно, синтезируемый оператор присвоения копии определяется как удаленный, если у класса есть ссылочный член.
Как будет продемонстрировано в разделах 13.6.2, 15.7.2 и 19.6, есть и другие аспекты, в связи с которыми функции-члены копирования могут быть определены как удаленные.
Как правило, функции-члены управления копированием синтезируются как удаленные, когда невозможно скопировать, присвоить или удалить член класса. Закрытые функции управления копированием
До появления нового стандарта классы предотвращали копирование, объявляя свой конструктор копий и оператор присвоения копии как закрытые (private): class PrivateCopy {
// нет спецификатора доступа; следующие члены являются закрытыми
// по умолчанию; см. p. 7.2
// функции управления копированием закрыты, а потому недоступны
// обычному пользовательскому коду
PrivateCopy(const PrivateCopy&);
PrivateCopy &operator=(const PrivateCopy&);
// другие члены public:
PrivateCopy() = default; // использовать синтезируемый стандартный
Page 636/1103
Чтобы определить эти функции-члены, сначала следует решить, что будет означать копирование объекта данного типа. Вообще, есть два способа: операцию копирования можно определить так, чтобы класс вел себя, как значение или как указатель.
У классов, которые ведут себя, как значения, есть собственное состояние. При копировании объекта как значения копия и оригинал независимы друг от друга. Внесенные в копию изменения никак не влияют на оригинал, и наоборот.
Классы, действующие как указатели, используют состояние совместно. При копировании объектов таких классов копии и оригиналы используют те же данные. Изменения, внесенные в копии, изменяют также оригинал, и наоборот.
Из использованных ранее библиотечных классов поведением, подобным значениям,
обладали классы библиотечных контейнеров и класс string. Ничего удивительного, что класс shared_ptr демонстрирует поведение, подобное указателю, как и класс StrBlob (см. раздел
12.1.1). Типы ввода-вывода и класс unique_ptr не допускают ни копирования, ни присвоения,
поэтому их поведение не похоже ни на значение, ни на указатель.
Чтобы проиллюстрировать эти два подхода, определим для используемого в упражнениях класса HasPtr функции-члены управления копированием. Сначала заставим класс действовать, как значение, а затем повторно реализуем его в версии, ведущей себя, как указатель.
У класса HasPtr есть два члена типа int и указатель на тип string. Обычно классы непосредственно копируют переменные-члены встроенного типа (кроме указателей); такие члены являются значениями, а следовательно, ведут себя обычно, как значения.
Происходящее при копировании указателя-члена определяет то, должно ли у такого класса,
как HasPtr, быть поведение, подобное значению или указателю. Упражнения раздела 13.2
Упражнение 13.22. Предположим, класс HasPtr должен вести себя, как значение. Таким образом, у каждого его объекта должна быть собственная копия строки, на которую указывает объект. Определения функций-членов управления копированием рассматривается в следующем разделе, но уже сейчас известно все необходимое для их реализации. Напишите конструктор копий класса HasPtr и оператор присвоения копии прежде, чем продолжите чтение.
13.2.1. Классы, действующие как значения
Для обеспечения поведения, подобного значению, у каждого объекта должна быть собственная копия ресурса, которым управляет класс. Это значит, что у каждого объекта класса HasPtr должна быть собственная копия строки, на которую указывает указатель ps.
Для реализации поведения, подобного значению, классу HasPtr нужно следующее.
• Конструктор копий, который копирует строку, а не только указатель.
• Деструктор, освобождающий строку.
• Оператор присвоения копии, освобождающий строку существующего объекта и копирующий
Page 638/1103
Вот подобная значению версия класса HasPtr: class HasPtr { public:
HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0) { }
// у каждого объекта класса HasPtr есть собственный экземпляр строки,
// на которую указывает указатель ps
HasPtr(const HasPtr &p) : ps(new std::string(*p.ps)), i(p.i) { }
HasPtr& operator=(const HasPtr &);
HasPtr() { delete ps; } private: std::string *ps; int i;
};
Класс достаточно прост, все, кроме оператора присвоения, определено в теле класса.
Первый конструктор получает (необязательный) аргумент типа string. Он динамически резервирует собственную копию этой строки и сохраняет ее адрес в указателе ps.
Конструктор копий также резервирует собственный экземпляр строки. Деструктор освобождает память, зарезервированную ее конструкторами, выполняя оператор delete для указателя-члена ps. Подобный значению оператор присвоения копии
Обычно операторы присвоения объединяют действия деструктора и конструктора копий.
Подобно деструктору, оператор присвоения освобождает ресурсы левого операнда. Подобно конструктору копий, оператор присвоения копирует данные из правого операнда. Однако критически важно, чтобы эти действия осуществлялись в правильной последовательности,
даже если объект присваивается сам себе. Кроме того, по возможности следует писать собственные операторы присвоения так, чтобы они оставляли левый операнд в корректном состоянии, иначе произойдет исключение (см. раздел 5.6.2).
В данном случае можно отработать случай присвоения самому себе (и сделать код устойчивым к исключению), осуществляя сначала копирование правого операнда. После копирования освобождается левый операнд и указатель модифицируется так, чтобы он указывал на вновь зарезервированную строку:
HasPtr& HasPtr::operator=(const HasPtr &rhs) {
Page 639/1103
};
В этом операторе присвоения, безусловно, сначала выполняется работа конструктора:
инициализатор newp идентичен инициализатору ps в конструкторе копий класса HasPtr.
Затем, как в деструкторе, удаляется строка, на которую в настоящее время указывает указатель ps. Остается только скопировать указатель на недавно созданную строку и значение типа int из rhs в этот объект. Ключевая концепция. Операторы присвоения
Создавая оператор присвоения, следует учитывать два момента.
• Операторы присвоения должны работать правильно, если объект присваивается сам себе.
• Большинство операторов присвоения делят работу с деструктором и конструктором копий.
Шаблон разработки оператора присвоения подразумевает сначала копирование правого операнда в локальный временный объект.
После копирования вполне безопасно удалить существующие члены левого операнда. Как только левый операнд будет освобожден, копировать данные из временного объекта в переменные-члены левого операнда.
Для иллюстрации важности принятия мер против присвоения самому себе рассмотрим, что случилось бы, выгляди оператор присвоения так:
//
НЕПРАВИЛЬНЫЙ способ написания оператора присвоения!
HasPtr&
HasPtr::operator=(const HasPtr &rhs) { delete ps; // освобождает строку, на которую указывает этот объект
// если rhs и *this - тот же объект, произойдет копирование удаленной
//
Page 640/1103
}
Если rhs и этот объект совпадают, удаление ps освободит строку, на которую указывают и
*this, и rhs. При попытке копирования *(rhs.ps) в операторе new этот указатель указывает уже на недопустимую область памяти. Результат непредсказуем.
Для операторов присвоения критически важно работать правильно, даже если объект присваивается сам себе. Проще всего обеспечить это, скопировав правый операнд перед удалением левого. Упражнения раздела 13.2.1
Упражнение 13.23. Сравните функции-члены управления копированием, написанные для решения упражнений предыдущего раздела, с кодом, представленным здесь. Убедитесь, что понимаете различия, если таковые вообще есть, между вашим кодом и приведенным в книге.
Упражнение 13.24. Что будет, если в версии класса HasPtr данного раздела не определен деструктор? Что если не определен конструктор копий?
Упражнение 13.25. Предположим, необходимо определить версию класса StrBlob,
действующего как значение. Предположим также, что необходимо продолжить использовать указатель shared_ptr, чтобы класс StrBlobPtr все еще мог использовать указатель weak_ptr для вектора. Переделанный класс будет нуждаться в конструкторе копий и операторе присвоения копии, но не в деструкторе. Объясните, что должны делать конструктор копий и оператор присвоения копий. Объясните, почему класс не нуждается в деструкторе.
Упражнение 13.26. Напишите собственную версию класса StrBlob, описанного в предыдущем упражнении.
13.2.2. Определение классов, действующих как указатели
Чтобы класс HasPtr действовал как указатель, конструктор копий и оператор присвоения копии должны копировать указатель-член, а не строку, на которую он указывает. Класс все еще будет нуждаться в собственном деструкторе, чтобы освободить память,
зарезервированную получающим строку конструктором (см. раздел 13.6). Тем не менее в данном случае деструктор не может односторонне освободить связанную с ним строку. Это можно сделать только тогда, когда исчезнет последний указатель на строку.
Простейший способ заставить класс действовать как указатель — это использовать указатель shared_ptr для управления ресурсами в классе. При копировании (или присвоении)
копируется (или присваивается) указатель shared_ptr. Класс shared_ptr сам отслеживает количество пользователей, совместно использующих объект, на который он указывает. Когда пользователей больше нет, класс shared_ptr освобождает ресурс.
Но иногда управлять ресурсом следует непосредственно. В таких случаях может пригодиться счетчик ссылок (reference count) (см. раздел 12.1.1). Для демонстрации работы счетчика
Page 641/1103
но с использованием собственного счетчика ссылок.Счетчики ссылок
Счетчик ссылок работает следующим образом.
• В дополнение к инициализации объекта каждый конструктор (кроме конструктора копий)
создает счетчик. Этот счетчик отслеживает количество объектов, совместно использующих создаваемые данные. Сразу после создания объект только один, поэтому счетчик инициализируется значением 1.
• Конструктор копий не создает новый счетчик; он копирует переменные-члены переданного ему объекта, включая счетчик. Конструктор копий увеличивает значение этого совместно используемого счетчика, указывая на наличие еще одного пользователя данных этого объекта.
• Деструктор уменьшает значение счетчика, указывая, что стало на одного пользователя совместно используемых данных меньше. Если значение счетчика достигает нуля,
деструктор удаляет данные.
• Оператор присвоения копии увеличивает счетчик правого операнда и уменьшает счетчик левого. Если счетчик левого операнда достигает нуля, значит, пользователей больше нет. В
данном случае оператор присвоения копии должен удалить данные левого операнда.
Единственное затруднение — это решить, где разместить счетчик ссылок. Счетчик не может быть членом непосредственно класса объекта HasPtr. Чтобы убедиться почему, рассмотрим происходящее в следующем примере:
HasPtr p1("Hiya!");
HasPtr p2(p1); // p1 и p2 указывают на ту же строку
HasPtr p3(p1); // p1, p2 и p3 указывают на ту же строку
Если счетчик ссылок будет храниться в каждом объекте, то как модифицировать его правильно при создании объекта p3? Можно увеличить счетчик в объекте p1 и скопировать счет в p3, но как модифицировать счетчик в p2?
Один из способов решения этой проблемы в том, чтобы хранить счетчик в динамической памяти. При создании объекта резервируется также и новый счетчик. При копировании или присвоении объекта копируется и указатель на счетчик. Таким образом, и копия, и оригинал укажут на тот же счетчик. Определение класса счетчика ссылок
Используя счетчик ссылок, можно написать подобную указателю версию класса HasPtr следующим образом: class HasPtr { public:
// конструктор резервирует новую строку и новый счетчик,
//
Page 642/1103
HasPtr(const std::string &s = std::string()): ps(new std::string(s)), i(0), use(new std::size_t(1)) {}
// конструктор копий копирует все три переменные-члена и увеличивает
// счетчик
HasPtr(const HasPtr &p): ps(p.ps), i(p.i), use(p.use) { ++*use; }
HasPtr& operator=(const HasPtr&);
HasPtr(); private: std::string *ps; int i; std::size_t *use; // член, отслеживающий количество объектов,
// совместно использующих *ps
};
Здесь была добавлена новая переменная-член use, отслеживающая количество объектов,
совместно использующих ту же строку. Получающий строку конструктор резервирует счетчик и инициализирует его значением 1, означающим наличие одного пользователя строкового члена класса этого объекта. Функции-члены копирования подобного указателю класса используют счетчик ссылок
При копировании или присвоении объектов класса HasPtr необходимо, чтобы копия и оригинал указывали на ту же строку. Таким образом, когда копируется объект класса HasPtr,
копируется сам указатель ps, а не строка, на которую он указывает. При копировании увеличивается также счетчик, связанный с этой строкой.
Конструктор копий (определенный в классе) копирует все три члена переданного ему объекта класса HasPtr. Этот конструктор увеличивает также значение указателя-члена use, означая,
что у строки, на которую указывают указатели ps и p.ps, появился другой пользователь.
Деструктор не может безоговорочно удалить указатель ps, поскольку могли бы быть и другие объекты, указывающие на ту же область памяти. Вместо этого деструктор осуществляет декремент счетчика ссылок, означая, что строку совместно используют на один объект меньше. Если счетчик достигает нуля, деструктор освобождает память, на которую указывают указатели ps и use:
Page 643/1103
} Упражнения раздела 13.2.2
Упражнение 13.27. Определите собственную версию класса HasPtr со счетчиком ссылок.
Упражнение 13.28. С учетом следующих классов реализуйте стандартный конструктор и необходимые функции-члены управления копированием.
(a) class TreeNode { (b) class BinStrTree { private: private: std::string value; TreeNode *root; int count; };
TreeNode *left;
TreeNode *right;
};
13.3. Функция swap()
Кроме функций-членов управления копированием, управляющие ресурсами классы зачастую определяют также функцию swap() (см. раздел 9.2.5). Определение функции swap() особенно важно для классов, которые планируется использовать с алгоритмами переупорядочивания элементов (см. раздел 10.2.3). Такие алгоритмы вызывают функцию swap() всякий раз, когда им нужен обмен двух элементов.
Если класс определяет собственную функцию swap(), алгоритм использует именно ее. В
противном случае используется функция swap(), определенная библиотекой. Как обычно,
хоть мы пока и не знаем, как реализуется функция swap(), концептуально несложно заметить,
что обмен двух объектов задействует копирование и два присвоения. Например, код обмена двух объектов подобного значению класса HasPtr (см. раздел 13.2.1) мог бы выглядеть так:
HasPtr temp = v1; // сделать временную копию значения v1 v1 = v2; // присвоить значение v2 объекту v1 v2 = temp; // присвоить сохраненное значение v1 объекту v2
Этот код дважды копирует строку, которая первоначально принадлежала объекту v1: один раз, когда конструктор копий класса HasPtr копирует объект v1 в объект temp, и второй раз,
когда оператор присвоения присваивает объект temp объекту v2. Он также копирует строку,
которая первоначально принадлежала объекту v2, когда объект v2 присваивается объекту v1.
Как уже упоминалось, копирование объекта, подобного значению класса HasPtr, резервирует
Page 645/1103
В принципе ни одно из этих резервирований памяти не обязательно. Вместо того чтобы резервировать новые копии строки, можно было бы обменять указатели. Таким образом,
имело бы смысл обменять два объект класса HasPtr так, чтобы выполнить следующее: string *temp = v1.ps; // создать временную копию указателя в v1.ps v1.ps = v2.ps; // присвоить указатель v2.ps указателю v1.ps v2.ps = temp; // присвоить сохраненный указатель v1.ps
// указателю v2.ps Написание собственной функции swap()
Переопределить стандартное поведение функции swap() можно, определив в классе ее собственную версию. Вот типичная реализация функции swap(): class HasPtr { friend void swap(HasPtr&, HasPtr&);
// другие члены, как в разделе 13.2.1
}; inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // обмен указателями, а не строковыми данными swap(lhs.i, rhs.i); // обмен целочисленными членами
}
Все начинается с объявления функции swap(), дружественной, чтобы предоставить ей доступ к закрытым переменным-членам класса HasPtr. Поскольку функция swap() предназначена для оптимизации кода, определим ее как встраиваемую (см. раздел 6.5.2). Тело функции swap()
вызывает функции swap() каждой из переменных-членов заданного объекта. В данном случае сначала обмениваются указатели, а затем целочисленные члены объектов, связанных с параметрами rhs и lhs.
В отличие от функций-членов управления копированием, функция swap() никогда не бывает
Page 646/1103
резервирующих ресурсы. Функции swap() должны вызвать функции swap(), а не std::swap()
В этом коде есть один важный нюанс: хотя в данном случае это не имеет значения, важно,
чтобы функция swap() вызвала именно функцию swap(), а не std::swap(). В классе HasPtr переменные-члены имеют встроенные типы. Для встроенных типов нет специализированных версий функции swap(). В данном случае она вызывает библиотечную функцию std::swap().
Но если класс имеет член, тип которого обладает собственной специализированной функцией swap(), то вызов функции std::swap() был бы ошибкой. Предположим, например, что есть другой класс по имени Foo, переменная-член h которого имеет тип HasPtr. Если не написать для класса Foo собственную версию функции swap(), то будет использована ее библиотечная версия. Как уже упоминалось, библиотечная функция swap() осуществляет ненужное копирование строк, управляемых объектами класса HasPtr.
Ненужного копирования можно избежать, написав функцию swap() для класса Foo. Но версию функции swap() для класса Foo можно написать так: void swap(Foo &lhs, Foo &rhs) {
//
Ошибка: эта функция использует библиотечную версию
// функции swap(), а не версию класса HasPtr std::swap(lhs.h, rhs.h); // обменять другие члены класса Foo
}
Этот код нормально компилируется и выполняется. Однако никакого различия в производительности между этим кодом и просто использующим стандартную версию функции swap() не будет. Проблема в том, что здесь явно запрошен вызов библиотечной версии функции swap(). Однако нужна версия функции не из пространства имен std, а определенная в классе HasPtr.
Правильный способ написания функции swap() приведен ниже. void swap(Foo &lhs, Foo &rhs) { using std::swap; swap(lhs.h, rhs.h); // использует функцию swap() класса HasPtr
// обменять другие члены класса Foo
}
Все вызовы функции swap() обходятся без квалификаторов. Таким образом, каждый вызов должен выглядеть как swap(), а не std::swap(). По причинам, рассматриваемым в разделе
Page 647/1103
// функции управления копированием, контролирующие указатели на
// это сообщение
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();
};
Page 651/1103
// в данную папку
}
Здесь происходит вызов функции 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() для удаления текущего сообщения.
Page 653/1103