Язык программирования C Пятое издание
Скачать 1.85 Mb.
|
Какую реализацию вы предпочитаете и почему? сразу после перемещения ресурса оригинальный объект больше не должен указывать на перемещенный ресурс, ответственность за него принимает вновь созданный объект. В качестве примера определим конструктор перемещения для класса StrVec, чтобы перемещать, а не копировать элементы из одного объекта класса StrVec в другой: StrVec::StrVec(StrVec &&s) noexcept // перемещение не будет передавать // исключений // инициализаторы членов получают ресурсы из s : elements(s.elements), first_free(s.first_free), cap(s.cap) { // оставить s в состоянии, при котором запуск деструктора безопасен s.elements = s.first_free = s.cap = nullptr; } Оператор noexcept (уведомляющий о том, что конструктор не передает исключений) описан ниже, а пока рассмотрим, что делает этот конструктор. В отличие от конструктора копий, конструктор перемещения не резервирует новую память; он получает ее от заданного объекта класса StrVec. Получив область памяти от своего аргумента, тело конструктора присваивает указателям заданного объекта значение nullptr. После перемещения оригинальный объект продолжает существовать. В конечном счете оригинальный объект будет удален, а значит, будет выполнен его деструктор. Деструктор класса StrVec вызывает функцию deallocate() для указателя first_free. Если забыть изменить указатель s.first_free, то удаление оригинального объекта освободит область памяти, которая была только что передана. Операции перемещения, библиотечные контейнеры и исключения Поскольку операция перемещения выполняется при "захвате" ресурсов, она обычно не резервирует ресурсы. В результате операции перемещения обычно не передают исключений. Когда создается функция перемещения, неспособная передавать исключения, об этом факте следует сообщить библиотеке. Как будет описано вскоре, если библиотека не знает, что конструктор перемещения не будет передавать исключений, она предпримет дополнительные меры по отработке возможности передачи исключения при перемещении объекта этого класса. Один из способов сообщить об этом библиотеке — определить оператор noexcept в конструкторе. Введенный новым стандартом оператор noexcept подробно рассматривается в разделе 18.1.4, а пока достаточно знать, что он позволяет уведомить, что функция не будет передавать исключений. Оператор noexcept указывают после списка параметров функции. В конструкторе его располагают между списком параметров и символом :, начинающим список инициализации конструктора: Page 669/1103 StrVec(StrVec&&) noexcept; // конструктор перемещения // другие члены, как прежде }; StrVec::StrVec(StrVec &&s) noexcept : /* инициализаторы членов */ { /* тело конструктора */ } Оператор noexcept следует объявить и в заголовке класса, и в определении, если оно расположено вне класса. Конструкторы перемещения и операторы присваивания при перемещении, которые не могут передавать исключения, должны быть отмечены как noexcept. Понимание того, почему необходим оператор noexcept, может помочь углубить понимание того, как библиотека взаимодействует с объектами написанных вами типов. В основе требования указывать, что функция перемещения не будет передавать исключения, лежат два взаимосвязанных факта: во- первых, хотя функции перемещения обычно не передают исключений, им это разрешено. Во-вторых, библиотечные контейнеры предоставляют гарантии относительно того, что они будут делать в случае исключения. Например, класс vector гарантирует, что, если исключение произойдет при вызове функции push_back(), сам вектор останется неизменным. Теперь рассмотрим происходящее в функции push_back(). Подобно соответствующей функции класса StrVec (см. раздел 13.5), функция push_back() класса vector могла бы потребовать пересоздания вектора. При пересоздании вектор перемещает элементы из прежней своей области памяти в новую, как в функции reallocate() (см. раздел 13.5). Как только что упоминалось, перемещение объекта обычно изменяет состояние оригинального объекта. Если пересоздание использует конструктор перемещения и этот конструктор передает исключение после перемещения некоторых, но не всех элементов, возникает проблема. Перемещенные элементов в прежнем пространстве были бы изменены, а незаполненные элементы в новом пространстве еще не будут созданы. В данном случае класс vector не удовлетворял бы требованию оставаться неизменным при исключении. С другой стороны, если класс vector использует конструктор копий, то при исключении он может легко удовлетворить это требование. В данном случае, пока элементы создаются в новой памяти, прежние элементы остаются неизменными. Если происходит исключение, вектор может освободить зарезервированное пространство (оно могло бы и не быть успешно зарезервировано) и прекратить операцию. Элементы оригинального вектора все еще существуют. Во избежание этой проблемы класс vector должен использовать во время пересоздания Page 670/1103 Оператор присваивания при перемещении делает то же, что и деструктор с конструктором перемещения. Подобно конструктору перемещения, если оператор присваивания при перемещении не будет передавать исключений, то его следует объявить как noexcept. Подобно оператору присвоения копии, оператор присваивания при перемещении должен принять меры против присвоения себя себе: StrVec &StrVec::operator=(StrVec &&rhs) noexcept { // прямая проверка на присвоение себя себе if (this != &rhs) { free(); // освободить существующие элементы elements = rhs.elements; // получить ресурсы от rhs first_free = rhs.first_free; cap = rhs.cap; // оставить rhs в удаляемом состоянии rhs.elements = rhs.first_free = rhs.cap = nullptr; } return *this; } В данном случае осуществляется прямая проверка совпадения адресов в указателях rhs и this. Если это так, то правый и левый операнды относятся к тому же объекту, и делать ничего не надо. В противном случае следует освободить память, которую использовал левый операнд, а затем принять память от заданного объекта. Как и в конструкторе перемещения, указателю rhs присваивается значение nullptr. Может показаться удивительным, что мы потрудились проверить присвоение себя самому. В конце концов, присваивание при перемещении требует для правого операнда r-значения. Проверка осуществляется потому, что то r-значение могло быть результатом вызова функции move(). Подобно любому другому оператору присвоения, крайне важно не освобождать ресурсы левого операнда прежде, чем использовать (возможно, те же) ресурсы правого Page 671/1103 Перемещение объекта не удаляет его оригинал: иногда после завершения операции перемещения оригинальный объект следует удалить. Поэтому, создавая функцию перемещения, следует гарантировать, что после перемещения оригинальный объект будет находиться в состоянии, допускающем запуск деструктора. Функция перемещения класса StrVec выполняет это требование и присваивает указателям-членам оригинального объекта значение nullptr. Кроме гарантии безопасного удаления оригинального объекта, функции перемещения должны оставлять объект в допустимом состоянии. Обычно допустимым считается тот объект, которому может быть безопасно присвоено новое значение или который может быть использован другими способами, не зависящими от его текущего значения. С другой стороны, у функций перемещения нет никаких требований относительно значения, которое остается в оригинальном объекте. Таким образом, программы никогда не должны зависеть от значения оригинального объекта после перемещения. Например, при перемещении объекта библиотечного класса string или контейнера известно, что оригинальный объект перемещения остается допустимым. В результате для оригинальных объектов перемещения можно выполнять такие функции, как empty() или size(). Однако предсказать результат их выполнения затруднительно. Логично было бы ожидать, что оригинальный объект перемещения будет пуст, но это не гарантируется. Функции перемещения класса StrVec оставляют оригинальный объект перемещения в том же состоянии, в котором он находился бы после инициализации по умолчанию. Поэтому все функции класса StrVec продолжат выполняться с его объектом точно так же, как с любым другим инициализированным по умолчанию объектом класса StrVec. Другие классы, с более сложной внутренней структурой, могут вести себя по-другому. После операции перемещения "оригинальный объект" должен остаться корректным, допускающим удаление объектом, но для пользователей его значение непредсказуемо. Синтезируемые функции перемещения Подобно конструктору копий и оператору присвоения копии, компилятор способен сам синтезировать конструктор перемещения и оператор присваивания при перемещении. Однако условия, при которых он синтезирует функции перемещения, весьма отличаются от тех, при которых он синтезирует функции копирования. Помните, что если не объявить собственный конструктор копий или оператор присвоения копии, компилятор всегда синтезирует их сам (см. раздел 13.1.1 и раздел 13.1.2). Функции копирования определяются или как функции почленного копирования либо присвоения объекта, или как удаленные функции. В отличие от функций копирования, для некоторых классов компилятор не синтезирует функции перемещения вообще . В частности, если класс определяет собственный конструктор копий, оператор присвоения копии или деструктор, конструктор перемещения и оператор присваивания при перемещении не синтезируются. В результате у некоторых классов нет конструктора перемещения или оператора присваивания при перемещении. Как будет продемонстрировано вскоре, когда у класса нет функции перемещения, вместо него в результате обычного подбора функции будет использована соответствующая функция копирования. Page 672/1103 • В отличие от конструктора копий, конструктор перемещения определяется как удаленный, если у класса есть член, определяющий собственный конструктор копий, но не определяющий конструктор перемещения, или если у класса есть член, который не определяет собственные функции копирования и для которого компилятор неспособен синтезировать конструктор перемещения. То же относится к присваиванию при перемещении. • Конструктор перемещения и оператор присваивания при перемещении определяются как удаленные, если у класса есть член, собственный конструктор перемещения которого или оператор присваивания при перемещении которого удален или недоступен. • Как и конструктор копий, конструктор перемещения определяется как удаленный, если деструктор удален или недоступен. • Как и оператор присвоения копии, оператор присваивания при перемещении определяется как удаленный, если у класса есть константный или ссылочный член. Предположим, например, что в классе Y определен собственный конструктор копий, но не определен собственный конструктор перемещения: // класс Y определяет собственный конструктор копий, но не конструктор // перемещения struct hasY { hasY() = default; hasY(hasY&&) = default; Y mem; // hasY будет иметь удаленный конструктор перемещения }; hasY hy, hy2 = std::move(hy); // ошибка: конструктор перемещения удален Компилятор может скопировать объекты типа Y, но не может переместить их. Класс hasY явно запросил конструктор перемещения, который компилятор не способен создать. Следовательно, класс hasY получит удаленный конструктор перемещения. Если бы у класса hasY отсутствовало объявление конструктора перемещения, то компилятор не синтезировал бы конструктор перемещения вообще. Функции перемещения не синтезируются, если в противном случае они были определены как удаленные. И последнее взаимоотношение между функциями перемещения и синтезируемыми функциями-членами управления копированием: тот факт, определяет ли класс собственные функции перемещения, влияет на то, как синтезируются функции копирования. Если класс определит любой конструктор перемещения и (или) оператор присваивания при перемещении, то синтезируемый конструктор копий и оператор присвоения копии для этого Page 674/1103 Классы, определяющие конструктор перемещения или оператор присваивания при перемещении, должны также определять собственные функции копирования. В противном случае эти функции-члены по умолчанию удаляются. R-значения перемещаются, а l-значения копируются… Когда у класса есть и конструктор перемещения и конструктор копий, компилятор использует обычный подбор функции, чтобы выяснить, какой из конструкторов использовать (см. раздел 6.4). С присвоением точно так же. Например, в классе StrVec версия копирования получает ссылку на const StrVec. В результате она применима к любому типу, допускающему приведение к классу StrVec. Версия перемещения получает StrVec&& и применима только к аргументам r-значениям (неконстантным): StrVec v1, v2; v1 = v2; // v2 - l-значение; присвоение копии StrVec getVec(istream &); // getVec возвращает r-значение v2 = getVec(cin); // getVec(cin) - r-значение; // присвоение перемещения В первом случае оператору присвоения передается объект v2. Его типом является StrVec, а выражение v2 является l-значением. Версия присвоения при перемещении не является подходящей (см. раздел 6.6), поскольку нельзя неявно связать ссылку на r-значение с l-значением. Следовательно, в этом случае используется оператор присвоения копии. Во втором случае присваивается результат вызова функции getVec(), — это r-значение. Теперь подходящими являются оба оператора присвоения — результат вызова функции getVec() можно связать с любым параметром оператора. Вызов оператора присвоения копии требует преобразования в константу, в то время как StrVec&& обеспечивает точное соответствие. Следовательно, второе присвоение использует оператор присваивания при перемещении. …но r-значения копируются, если нет конструктора перемещения Что если класс имеет конструктор копий, но не определяет конструктор перемещения? В данном случае компилятор не будет синтезировать конструктор перемещения. Это значит, что у класса есть конструктор копий, но нет конструктора перемещения. Если у класса нет конструктора перемещения, подбор функции гарантирует, что объекты этого типа будут копироваться, даже при попытке перемещения их вызовом функции move(): class Foo { public: Foo() = default; Foo(const Foo&); // Page 675/1103 // другие члены, но Foo не определяет конструктор перемещения }; Foo x; Foo y(x); // конструктор копий; x - это l-значение Foo z(std::move(x)); // конструктор копий, поскольку конструктора // перемещения нет Вызов функции move(x) при инициализации объекта z возвращает указатель Foo&&, привязанный к объекту x. Конструктор копий для класса Foo является подходящим, поскольку вполне допустимо преобразовать Foo&& в const Foo&. Таким образом, инициализация объекта z использует конструктор копий класса Foo. Следует заметить, что использование конструктора копий вместо конструктора перемещения почти безусловно безопасно (то же справедливо и для оператора присвоения). Обычно конструктор копий отвечает требованиям соответствующего конструктора перемещения: он копирует заданный объект и оставляет оригинальный объект в допустимом состоянии. Конструктор копий, напротив, не будет изменять значение оригинального объекта. Если у класса будет пригодный конструктор копий и не будет конструктора перемещения, то объекты будут перемещены конструктором копий. То же справедливо для оператора присвоения копии и присвоения при перемещении. Операторы присвоения копии и обмена и перемещение Версия класса HasPtr, определявшая оператор присвоения копии и обмена (copy-and-swap assignment operator) (см. раздел 13.3), — хорошая иллюстрация взаимодействия механизма подбора функции и функций перемещения. Если в этот класс добавить конструктор перемещения, то фактически будет получен также оператор присваивания при перемещении: class HasPtr { public: // добавлен конструктор перемещения HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;} // оператор присвоения - и оператор перемещения, и присвоения копии Page 676/1103 13.6). Как правило, у таких классов есть ресурс, который должны копировать функции-члены копирования. Обычно копирование ресурса влечет за собой некоторые дополнительные затраты. Классы, определяющие конструктор перемещения и оператор присваивания при перемещении, могут избежать этих затрат в тех обстоятельствах, где копия не обязательна.Функции перемещения для класса Message Классы, определяющие собственный конструктор копий и оператор присвоения копии, обычно определяют и функции перемещения. Например, наши классы Message и Folder (см. раздел 13.4), должны определять функции перемещения. При определении функций перемещения класс Message может использовать функции перемещения классов string и set, чтобы избежать дополнительных затрат при копировании членов contents и folders. Но в дополнение к перемещению члена folders следует также обновить каждый объект класса Folder, указывавший на оригинал объекта класса Message. Следует также удалить указатели на прежний объект класса Message и добавить указатели на новый. И конструктор перемещения, и оператор присваивания при перемещении должны обновлять указатели Folder, поэтому начнем с определения функций для выполнения этих действий: // переместить указатели Folder из m в данное Message void Message::move_Folders(Message *m) { folders = std::move(m->folders); // использует присвоение перемещения // класса set for (auto f : folders) { // для каждого Folder f->remMsg(m); // удалить старый Message из Folder f->addMsg(this); // добавить этот Message в этот Folder } m->folders.clear(); // гарантировать безопасное удаление m } Функция начинает работу с перемещения набора folders. При вызове функции move() используется оператор присвоения при перемещении класса set, а не его оператор присвоения копии. Если пропустить вызов функции move(), код все равно будет работать, но осуществляя ненужное копирование. Затем функция перебирает папки, удаляя указатель на Page 678/1103 Следует заметить, что вставка элемента в набор может привести к передаче исключения, поскольку добавление элемента на контейнер требует резервирования памяти, вполне может быть передано исключение bad_alloc (см. раздел 12.1.2). Таким образом, в отличие от функций перемещения классов HasPtr и StrVec, конструктор перемещения и операторы присваивания при перемещении класса Message могли бы передать исключения, поэтому не будем отмечать их как noexcept (см. раздел 13.6.2). Функция заканчивается вызовом функции clear() объекта m.folders. Известно, что после перемещения объект m.folders вполне допустим, но его содержимое непредсказуемо. Поскольку деструктор класса Message перебирает набор folders, необходимо убедиться, что набор пуст. Конструктор перемещения класса Message вызывает функцию move(), чтобы переместить содержимое и инициализировать по умолчанию свой член folders: Message::Message(Message &&m): contents(std::move(m.contents)) { move_Folders(&m); // переместить folders и обновить указатели Folder } В теле конструктора происходит вызов функции move_Folders(), чтобы удалить указатели на m и вставить указатели на данное сообщение. Оператор присваивания при перемещении непосредственно проверяет случай присвоения себя себе: Messages Message::operator=(Message &&rhs) { if (this != &rhs) { // прямая проверка присвоения себя себе remove_from_Folders(); contents = std::move(rhs.contents); // присвоение при перемещении move_Folders(&rhs); // сбросить папки, чтобы указывать на это // сообщение } return *this; } Подобно любым операторам присвоения, оператор присваивания при перемещении должен Page 679/1103 чтобы переместить contents из объекта rhs в this. Остается только вызвать функцию move_Folders(), чтобы модифицировать указатели Folder. Итераторы перемещения Функция reallocate() класса StrVec (см. раздел 13.5) использовала вызов функции construct() в цикле for для копирования элементов из прежней памяти в новую. Альтернативой циклу был бы просто вызов функции uninitialized_copy() для создания нового пространства в памяти. Однако функция uninitialized_copy() делает именно то, о чем говорит ее имя: она копирует элементы. Нет никакой аналогичной библиотечной функции для перемещения объектов в пустую память. Вместо нее новая библиотека определяет адаптер итератора перемещения (move iterator) (см. раздел 10.4). Итератор перемещения адаптирует переданный ему итератор, изменяя поведение его оператора обращения к значению. Обычно оператор обращения к значению итератора возвращает ссылку на l-значение элемента. В отличие от других итераторов, оператор обращения к значению итератора перемещения возвращает ссылку на r-значение. Обычный итератор преобразуется в итератор перемещения при вызове библиотечной функции make_move_iterator(), которая получает итератор и возвращает итератор перемещения. Все остальные функции первоначального итератора работают, как обычно. Поскольку эти итераторы поддерживают обычные функции итераторов, пару итераторов перемещения вполне можно передать алгоритму. В частности, итераторы перемещения можно передать алгоритму uninitialized_copy(): void StrVec::reallocate() { // зарезервировать вдвое больше пространства, чем для текущего // количества элементов auto newcapacity = size() ? 2 * size() : 1; auto first = alloc.allocate(newcapacity); // переместить элементы auto last = uninitialized_copy(make_move_iterator(begin()), make_move_iterator(end()), first); free(); // освободить прежнее пространство Page 680/1103 } Алгоритм uninitialized_copy() вызывает функцию construct() для каждого элемента исходной последовательности, чтобы скопировать элемент по назначению. Для выбора элемента из исходной последовательности данный алгоритм использует оператор обращения к значению итератора. Поскольку был передан итератор перемещения, оператор обращения к значению возвращает ссылку на r-значение. Это означает, что функция construct() будет использовать для создания элементов конструктор перемещения. Следует заметить, что стандартная библиотека не дает гарантий применимости всех алгоритмов с итераторами перемещения. Так как перемещение объекта способно удалить оригинал, итераторы перемещения следует передать алгоритмам, только тогда, когда вы уверены, что алгоритм не будет обращаться к элементам после того, как он присвоил этот элемент или передал его пользовательской функции. Совет. Не слишком спешите с перемещением Поскольку состояние оригинального объекта перемещения неопределенно, вызов для него функции std::move() — опасная операция. Когда происходит вызов функции move(), следует быть абсолютно уверенным в том, что у оригинального объекта перемещения не может быть никаких других пользователей. Взвешенно использованная в коде класса, функция move() способна обеспечить существенный выигрыш в производительности. Небрежное ее использование в обычном пользовательском коде (в отличие от кода реализации класса), вероятней всего, приведет к загадочным и трудно обнаруживаемым ошибкам, а не к повышению производительности приложения. За пределами кода реализации класса, такого как конструкторы перемещения или операторы присваивания при перемещении, используйте функцию std::move() только при абсолютной уверенности в необходимости перемещения и в том, что перемещение гарантированно будет безопасным. Упражнения раздела 13.6.2 Упражнение 13.49. Добавьте конструктор перемещения и оператор присваивания при перемещении в классы StrVec, String и Message. Упражнение 13.50. Снабдите функции перемещения класса String операторами вывода и снова запустите программу из упражнения 13.48 раздела 13.6.1, в котором использовался вектор vector<String>, и посмотрите, когда теперь удается избежать копирования. Упражнение 13.51. Хотя указатель unique_ptr не может быть скопирован, в разделе 12.1.5 была написана функция clone(), которая возвратила указатель unique_ptr по значению. Объясните, почему эта функция допустима и как она работает. Упражнение 13.52. Объясните подробно, что происходит при присвоении объектов класса HasPtr. В частности, опишите шаг за шагом, что происходит со значениями hp, hp2 и параметром rhs в операторе присвоения класса HasPtr. Упражнение 13.53. С точки зрения низкоуровневой эффективности оператор присвоения класса HasPtr не идеален. Объясните почему. Реализуйте для класса HasPtr оператор Page 681/1103 Упражнение 13.54. Что бы случилось, если бы мы определи оператор присваивания при перемещении для класса HasPtr, но не изменили оператор копии и обмена? Напишите код для проверки вашего ответа. 13.6.3. Ссылки на r-значение и функции-члены Все функции-члены, кроме конструкторов и операторов присвоения, могут извлечь пользу из предоставления версии копирования и перемещения. Такие функции-члены с поддержкой перемещения обычно используют ту же схему параметров, что и конструктор копий/перемещения и операторы присвоения, — одна версия получает ссылку на константное l-значение, а вторая — ссылку на не константное r-значение. Например, библиотечные контейнеры, определяющие функцию push_back(), предоставляют две версии: параметр одной является ссылкой на r-значение, а другой — ссылкой на константное l-значение. С учетом того, что X является типом элемента, эти функции контейнера определяются так: void push_back(const X&); // копирование: привязка к любому X void push_back(X&&); // перемещение: привязка только к изменяемым // r-значениям типа X Первой версии функции push_back() можно передать любой объект, который может быть приведен к типу X. Эта версия копирует данные своего параметра. Второй версии можно передать только r-значение, которое не является константой. Эта версия точнее и лучшее соответствует неконстантным r-значениям и будет выполнена при передаче поддающегося изменению r-значения (см. раздел 13.6.2). Эта версия способна захватить ресурсы своего параметра. Обычно нет никакой необходимости определять версии функций получающих const X&& или просто X&. Обычно ссылку на r-значение передают при необходимости "захватить" аргумент. Для этого аргумент не должен быть константой. Точно так же копирование объекта не должно изменять скопированный объект. В результате обычно нет никакой необходимости определять версию, получающую простой параметр X&. У перегруженных функций, различающих перемещение и копирование параметра, обычно есть одна версия, получающая параметр типа const Т&, и вторая, получающая параметр типа T&&. В качестве более конкретного примера придадим классу StrVec вторую версию функции push_back(): class StrVec { Page 682/1103 // другие члены как прежде }; // неизменно с оригинальной версии в разделе 13.5 void StrVec::push_back(const string& s) { chk_n_alloc(); // удостовериться в наличии места для другого элемента // создать копию s в элементе, на который указывает first_free alloc.construct(first_free++, s); } void StrVec::push_back(string &&s) { chk_n_alloc(); // пересоздает StrVec при необходимости alloc.construct(first_free++, std::move(s)); } Эти функции-члены почти идентичны. Различие в том, что версия ссылки на r-значение функции push_back() вызывает функцию move(), чтобы передать этот параметр функции construct(). Как уже упоминалось, функция construct() использует тип своего второго и последующих аргументов для определения используемого конструктора. Поскольку функция move() возвращает ссылку на r-значение, аргумент функции construct() будет иметь тип string&&. Поэтому для создания нового последнего элемента будет использован конструктор перемещения класса string. Когда вызывается функция push_back(), тип аргумента определяет, копируется ли новый элемент в контейнер или перемещается: StrVec vec; // пустой StrVec Page 683/1103 Эти вызовы различаются тем, является ли аргумент l-значением (s) или r-значением (временная строка, созданная из слова "done"). Вызовы распознаются соответственно. Ссылки на l-значения, r-значения и функции-члены Обычно функцию-член объекта можно вызвать независимо от того, является ли этот объект l- или r-значением. Например: string s1 = "a value", s2 = "another"; auto n = (s1 + s2).find('a'); Здесь происходит вызов функции-члена find() (см. раздел 9.5.3) для r-значения класса string, полученного при конкатенации двух строк. Иногда такой способ применения может удивить: s1 + s2 = "wow!"; Здесь r-значению присваивается результат конкатенации двух строк. До нового стандарта не было никакого способа предотвратить подобное применение. Для обеспечения совместимости с прежней версией библиотечные классы продолжают поддерживать присвоение r-значению; в собственных классах такое может понадобиться предотвратить. В таком случае левый операнд (т.е. объект, на который указывает указатель this) обязан быть l-значением. Свойство l- или r-значения указателя this задают таким же образом, как и константность функции-члена (см. раздел 7.1.2): помещая квалификатор ссылки (reference qualifier) после списка параметров: class Foo { public: Foo &operator=(const Foo&) &; // возможно присвоение только // изменяемым l-значениям // другие члены класса Foo }; Foo &Foo::operator=(const Foo &rhs) & { Page 684/1103 Foo Foo::sorted() const & { Foo ret(*this); // создает копию sort(ret.data.begin(), ret.data.end()); // сортирует копию return ret; // возвращает копию } При выполнении функции sorted() для r-значения вполне безопасно сортировать вектор-член data непосредственно. Объект является r-значением, а это означает, что у него нет никаких других пользователей, поэтому данный объект можно изменить непосредственно. При выполнении функции sorted() для константного r- или l-значения изменить этот объект нельзя, поэтому перед сортировкой вектор-член data необходимо скопировать. Поиск перегруженной функции использует свойство l-значение/r-значение объекта, вызвавшего функцию sorted() для определения используемой версии: retVal().sorted(); // retVal() - это r-value, вызов Foo::sorted() && retFoo().sorted(); // retFoo() - это l-value, // вызов Foo::sorted() const & При определении константных функций-членов можно определить две версии, отличающиеся только тем, что одна имеет квалификатор const, а другая нет. Для ссылочной квалификации функций ничего подобного по умолчанию нет. При определении двух или более функций-членов с тем же именем и тем же списком параметров следует предоставить квалификатор ссылки для всех или ни для одной из этих функций: class Foo { public: Foo sorted() &&; Foo sorted() const; // ошибка: должен быть квалификатор ссылки // Comp - псевдоним для типа функции (см. p. 6.7) Page 687/1103 Копирование и обмен (copy and swap). Техника написания операторов присвоения за счет копирования правого операнда, сопровождаемого вызовом функции swap(), обменивающей копию с левым операндом. Оператор присваивания при перемещении (move-assignment operator). Версия оператора присвоения, получающая ссылку r-значения на ее тип. Как правило, оператор присваивания при перемещении перемещает данные из правого операнда в левый. После присвоения запуск деструктора для правого операнда должен быть безопасен. Оператор присвоения копии (copy-assignment operator). Версия оператора присвоения, получающая объект того же типа, что и у нее. Обычно оператор присвоения копии имеет параметр, являющийся ссылкой на константу, и возвращает ссылку на свой объект. Компилятор сам синтезирует оператор присвоения копии, если класс не предоставляет его явно. Перегруженный оператор (overloaded operator). Функция, переопределяющая один из операторов для работы с операндами данного класса. В этой главе описано определение лишь оператора присвоения, а более подробно перегрузка операторов рассматривается в главе 14. Почленное копирование и присвоение (memberwise copy/assign). Так работают синтезируемые конструкторы копирования и перемещения, а также операторы присваивания при перемещении и копи. Перебирая все нестатические переменные-члены по очереди, синтезируемый конструктор копий или перемещения инициализирует каждую из них, копируя или при перемещая соответствующее значение из заданного объекта; оператор присваивания при перемещении и копии присваивают при перемещении или копируют каждую переменную-член правого объекта в левый. Инициализация и присвоение переменных-членов встроенного или составного типа осуществляются непосредственно, а членов типа класса — с использованием соответствующего конструктора перемещения или копирования либо оператора присвоения копии или присваивания при перемещении. Синтезируемые конструкторы копирования и перемещения (synthesized copy/move constructor). Версии конструкторов копирования и перемещения, синтезируемые компилятором для классов, которые не определяют соответствующие конструкторы явно. Если они не определены как удаленные функции, синтезируемые конструкторы копирования и перемещения почленно инициализируют новый объект, копируя или перемещая члены из заданного объекта. Синтезируемый деструктор (synthesized destructor). Версия деструктора, создаваемая (синтезируемая) компилятором для классов, в которых он не определен явно. Тело синтезируемого деструктора пусто. Синтезируемый оператор присвоения (synthesized assignment operator). Версия оператора присвоения, создаваемого (синтезируемого) компилятором для классов, у которых он не определен явно. Если он не определен как удаленная функция, синтезируемый оператор присвоения почленно присваивает (перемещает) правый операнд левому. Ссылка на l-значение (l-value reference). Ссылка, которая может быть связана с l-значением. Ссылка на r-значение (r-value reference). Ссылка на объект, который будет удален. Счетчик ссылок (reference count). Программное средство, обычно используемое в членах управления копированием. Счетчик ссылок отслеживает количество объектов, совместно использующих некую сущность. Конструкторы (кроме конструкторов копирования и Page 690/1103 значение счетчика увеличивается. Когда объект удаляется, значение счетчика уменьшается. Оператор присвоения и деструктор проверяют, не достиг ли декремент счетчика ссылок нуля, и если это так, то они удаляют объект. Удаленная функция (deleted function). Функция, которая не может быть использована. Для удаления функции в ее объявление включают часть = delete. Обычно удаленные функции используют для запрета компилятору синтезировать операторы копирования и (или) перемещения для класса. Управление копированием (copy control). Специальные функции-члены, которые определяют действия, осуществляемые при копировании, присвоении и удалении объектов класса. Если эти функции не определены в классе явно, компилятор синтезирует их самостоятельно. Функция move(). Библиотечная функция, обычно используемая для связи ссылки r-значения с l-значением. Вызов функции move() неявно обещает, что объект не будет использован для перемещения, кроме его удаления или присвоения нового значения. Глава 14 Перегрузка операторов и преобразований Как упоминалось в главе 4, язык С++ предоставляет для встроенных типов множество операторов и автоматических преобразований. Они позволяют создавать разнообразные выражения, где используются разные типы данных. Язык С++ позволяет переопределять смысл операторов, применяемых для объектов типа класса, а также определять для класса функции преобразования типов. Функции преобразования типа класса используются подобно встроенным преобразованиям для неявного преобразования (при необходимости) объекта одного типа в другой. Перегрузка оператора (overloaded operator) позволяет определить смысл оператора, когда он применяется к операнду (операндам) типа класса. Разумное применение перегрузки операторов способно упростить программы, облегчить их написание и чтение. Например, поскольку наш первоначальный класс Sales_item (см. раздел 1.5.1) определял операторы ввода, вывода и суммы, сумму двух объектов класса Sales_item можно вывести так: cout << item1 + item2; // вывод суммы двух объектов класса Sales_item Класс Sales_data (см. раздел 7.1), напротив, еще не имеет перегруженных операторов, поэтому код вывода суммы его объектов окажется более подробным, а следовательно, менее ясным: print(cout, add(data1, data2)); // вывод суммы двух объектов // класса Sales_data Page 691/1103 При проектировании перегруженных операторов необходимо принять решение, должен ли каждый из них быть членом класса или обычной функцией (не членом класса). В некоторых случаях выбора нет; оператор должен быть членом класса. В других случаях можно принять во внимание несколько эмпирических правил, которые помогут принять решение. Приведенный ниже список критериев может оказаться полезен в ходе принятия решения о том, следует ли сделать оператор функцией-членом класса или обычной функцией. • Операторы присвоения (=), индексирования ([]), вызова (()) и доступа к члену класса (->) следует определять как функции-члены класса. • Подобно оператору присвоения, составные операторы присвоения обычно должны быть членами класса. Но в отличие от оператора присвоения, это не обязательно. • Другие операторы, которые изменяют состояние своего объекта или жестко связаны с данным классом (например, инкремент, декремент и обращение к значению), обычно должны быть членами класса. • Симметричные операторы, такие как арифметические, операторы равенства, операторы сравнения и побитовые операторы, лучше определять как обычные функции, а не члены класса. Разработчики ожидают возможности использовать симметричные операторы в выражениях со смешанными типами. Например, возможности сложить переменные типа int и double. Сложение симметрично, а потому можно использовать тип как левого, так и правого операнда. Если необходимо обеспечить подобные выражения смешанного типа, задействующие объекты класса, то оператор должен быть определен как функция, не являющаяся членом класса. При определении оператора как функции-члена левый операнд должен быть объектом того класса, членом которого является этот оператор. Например: string s = "world"; string t = s + "!"; // ok: const char* можно добавить к строке string u = "hi" + s; // возможна ошибка, если + будет членом // класса string Если бы оператор operator+ был членом класса string, то первый случай сложения был бы эквивалентен s.operator+("!"). Аналогично сложение "hi" + s было бы эквивалентно Page 695/1103 |