Главная страница

Язык программирования C Пятое издание


Скачать 1.85 Mb.
НазваниеЯзык программирования C Пятое издание
Дата15.07.2019
Размер1.85 Mb.
Формат файлаpdf
Имя файла620354-www.libfox.ru.pdf
ТипДокументы
#84130
страница37 из 54
1   ...   33   34   35   36   37   38   39   40   ...   54

Какую реализацию вы предпочитаете и почему?
Упражнение 13.44. Напишите класс по имени String, являющийся упрощенной версией библиотечного класса string. У вашего класса должен быть по крайней мере стандартный конструктор и конструктор, получающий указатель на строку в стиле С. Примените для резервирования используемой классом String памяти класс allocator.
13.6. Перемещение объектов
Одной из главных особенностей нового стандарта является способность перемещать объект,
а не копировать. Как упоминалось в разделе 13.1.1, копирование осуществляется при многих обстоятельствах. При некоторых из них объект разрушается немедленно после копирования.
В этих случаях перемещение объекта вместо копирования способно обеспечить существенное увеличение производительности.
Как было продемонстрировано только что, наш класс StrVec — хороший пример лишнего копирования. Во время пересоздания нет никакой необходимости в копировании элементов из старой памяти в новую, лучше перемещение. Вторая причина предпочесть перемещение копированию — это такие классы как unique_ptr и классы ввода-вывода. У этих классов есть ресурс (такой как указатель или буфер ввода-вывода), который не допускает совместного использования. Следовательно, объекты этих типов не могут быть скопированы, но могут быть перемещены.
В прежних версиях языка не было непосредственного способа перемещения объекта. Копию приходилось делать, даже если в этом не было никакой потребности. Когда объекты велики или когда они требуют резервирования памяти (например, строки), бесполезное копирование может обойтись очень дорого. Точно так же в предыдущих версиях библиотеки классы хранимых в контейнере объектов должны были допускать копирование. По новому стандарту в контейнерах можно хранить объекты типов, которые не допускают копирования, но могут быть перемещены.
Контейнеры библиотечных типов, классы string и shared_ptr поддерживают как перемещение,
так и копирование. Классы ввода-вывода и класс unique_ptr допускают перемещение, но не копирование.
13.6.1. Ссылки на r-значение
Page 665/1103

Для обеспечения операции пересылки, новый стандарт вводит новый вид ссылок — ссылки на r-значение.
Ссылка на r-значение (r-value reference) — это ссылка, которая должна быть связана с r-значением. Ссылку на r-значение получают с использованием символа &&, а не
&. Как будет продемонстрировано далее, у ссылок на r-значение есть важное свойство —
они могут быть связаны только с тем объектом, который будет удален. В результате можно "перемещать" ресурсы от ссылки на r-значение в другой объект.
Напомним, что l- и r-значение — свойства выражения (см. раздел 4.1.1). Некоторые выражения возвращают или требуют l-значений; другие возвращают или требуют r-значений.
Как правило, выражение l-значения относится к идентификатору объекта, тогда как выражение r-значения — к значению объекта.
Как и любая ссылка, ссылка на r-значение — это только другое имя для объекта. Как известно, нельзя связать обычные ссылки (которые далее будем называть ссылками на l-значение (l-value reference), чтобы отличить их от ссылок на r-значения) с выражениями, требующими преобразования, с литералами и с выражениями, которые возвращают r-значение (см. раздел 2.3.1). У ссылок на r-значение противоположные свойства привязки: можно связать ссылку на r-значение с выражениями, приведенными выше, но нельзя непосредственно связать ссылку на r-значение с l-значением: int i = 42; int &r = i; // ok: r ссылается на i int &&rr = i; // ошибка: нельзя связать ссылку на r-значение
// с l-значением int &r2 = i * 42; // ошибка: i * 42 - это r-значение const int &r3 = i * 42; // ok: ссылку на константу можно
// связать с r-значением int &&rr2 = i * 42; // ok: связать rr2 с результатом умножения
Функции, возвращающие ссылки на l-значение, наряду с присвоением, индексированием,
обращением к значению, а также префиксные операторы инкремента и декремента являются примерами выражений, возвращающих l-значения. Ссылку на l-значение можно также связать с результатом любого из этих выражений.
Page 666/1103

Все функции, возвращающие не ссылочный тип, наряду с арифметическими, реляционными,
побитовыми и постфиксными операторами инкремента и декремента возвращают r-значения.
С этими выражениями нельзя связать ссылку на l-значение, но можно связать либо константную ссылку на l-значение, либо ссылку на r-значение. l-значения — устойчивы;
r-значения — эфемерны
Глядя на список выражений l- и r-значений, становится понятно, что l- и r-значения существенно отличаются друг от друга: у l-значений есть постоянное состояние, тогда как r-значения, литералы и временные объекты создаются лишь в ходе вычисления выражений.
Поскольку ссылки на r-значение могут быть связаны только с временным объектом, известно,
что:
• упомянутый объект будет удален,
• у этого объекта не может быть других пользователей.
Совместно эти факты означают, что использующий ссылку на r-значение код способен получать ресурсы от объекта, на который ссылается ссылка.
Ссылки на r-значение ссылаются на объекты, которые будут вскоре удалены. Следовательно,
можно "захватить" состояние объекта, связанного со ссылкой на r-значение. Переменные являются l-значениями
Хотя мы редко думаем об этом, переменная — это выражение с одним операндом и без оператора. Подобно любому другому выражению, переменная как выражение имеет свойства l- и r-значения. Переменные как выражения — это l-значения. Удивительно, но как следствие невозможно связать ссылку на r-значение с переменной, определенной как тип ссылки на r-значение: int &&rr1 = 42; // ok: литералы - это r-значения int &&rr2 = rr1; // ошибка: выражение rr1 - это l-значение!
С учетом предыдущего наблюдения, согласно которому r-значения представляют эфемерные объекты, нет ничего удивительного в том, что переменная представляет собой l-значение. В
конце концов, переменная сохраняется, пока не выйдет из области видимости.
Переменная — это l-значение; нельзя непосредственно связать ссылку на r-значение с переменной, даже если эта переменная была определена как тип ссылки на r-значение .Библиотечная функция move()
Хотя нельзя непосредственно связать ссылку на r-значение с l-значением, можно явно привести l-значение к соответствующему типу ссылки на r-значение. Вызов новой библиотечной функции move(), определенной в заголовке utility, позволяет также получить ссылку на r-значение, привязанную к l-значению. Для возвращения ссылки на r-значение на данный объект функция move() использует средства, описываемые в разделе 16.2.6: int &&rr3 = std::move(rr1); // ok
Page 667/1103

Вызов функции move() указывает компилятору, что имеющееся l-значение следует рассматривать как r-значение. Следует помнить, что приведенный выше вызов функции move() обещает не использовать rr1 ни для чего, кроме присвоения или удаления. После вызова функции move() нельзя сделать никаких предположений о значении уже перемещенного объекта.
Перемещенный объект можно удалить, а можно присвоить ему новое значение, но значение уже перемещенного объекта использовать нельзя.
Как уже упоминалось, для использования большинства имен из библиотеки, включая функцию move() (см. раздел 13.5), не нужно предоставлять объявление using (см. раздел 3.1).
Произойдет вызов функции std::move(), а не move(). Причины этого рассматриваются в разделе 18.2.3.
Код, применяющий функцию move(), должен использовать синтаксис std::move(), а не move().
Это позволит избежать возможных конфликтов имен. Упражнения раздела 13.6.1
Упражнение 13.45. В чем разница между ссылкой на r-значение и ссылкой на l-значение.
Упражнение 13.46. Какой вид ссылки может быть связан со следующими инициализаторами? int f(); vector<int> vi(100); int? r1 = f(); int? r2 = vi[0] ; int? r3 = r1; int? r4 = vi[0] * f();
Упражнение 13.47. Снабдите конструктором копий и оператором присвоения копии класса
String из упражнения 13.44 раздела 13.5, функции которого выводят сообщения при каждом вызове.
Упражнение 13.48. Определите вектор vector<String> и вызовите для него функцию push_back() несколько раз. Запустите программу и посмотрите, как часто копируются строки.
13.6.2. Конструктор перемещения и присваивание при перемещении
Подобно классу string (и другим библиотечным классам), наши собственные классы могут извлечь пользу из способности перемещения ресурсов вместо копирования. Чтобы позволить собственным типам операции перемещения, следует определить конструктор перемещения и оператор присваивания при перемещении. Эти члены подобны соответствующим функциям копирования, но они захватывают ресурсы заданного объекта, а не копируют их.
Как и у конструктора копий, у конструктора перемещения есть начальный параметр,
являющийся ссылкой на тип класса. В отличие от конструктора копии, ссылочный параметр конструктора перемещения является ссылкой на r-значение. Подобно конструктору копий, у всех дополнительных параметров должны быть аргументы по умолчанию.
Кроме перемещения ресурсов, конструктор перемещения должен гарантировать такое
Page 668/1103
состояние перемещенного объекта, при котором его удаление будет безопасно. В частности,
сразу после перемещения ресурса оригинальный объект больше не должен указывать на перемещенный ресурс, ответственность за него принимает вновь созданный объект.
В качестве примера определим конструктор перемещения для класса 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
class StrVec { public:
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.Оператор присваивания при перемещении
Оператор присваивания при перемещении делает то же, что и деструктор с конструктором перемещения. Подобно конструктору перемещения, если оператор присваивания при перемещении не будет передавать исключений, то его следует объявить как 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

Компилятор синтезирует конструктор перемещения или оператор присваивания при перемещении, только если класс не определяет ни одной из собственных функций-членов управления копированием и если каждая нестатическая переменная-член класса может быть перемещена. Компилятор может перемещать члены встроенного типа, а также члены типа класса, если у него есть соответствующая функция-член перемещения:
// компилятор синтезирует функции перемещения для X и hasX struct X { int i; // встроенные типы могут быть перемещены std::string s; // string определяет собственные функции перемещения
}; struct hasX {
X mem; // для X синтезированы функции перемещения
};
X x, х2 = std::move(x); // использует синтезируемый конструктор
// перемещения hasX hx, hx2 = std::move(hx); // использует синтезируемый конструктор
// перемещения
Компилятор синтезирует конструктор перемещения и оператор присваивания при перемещении, только если класс не определяет ни одной из собственных функций-членов управления копированием и только если все переменные-члены могут быть созданы перемещением и присвоены при перемещении соответственно.
• В отличие от функций копирования, функции перемещения никогда не определяются неявно как удаленные. Но если явно запросить компилятор создать функцию перемещения,
применив = default (см. раздел 7.1.4), но компилятор окажется неспособен переместить все члены, то функция перемещения будет определена как удаленная. Важное исключение из правила, согласно которому синтезируемая функция перемещения определяется как
Page 673/1103
удаленная, подобно таковому для функций копирования (см. раздел 13.1.6).
• В отличие от конструктора копий, конструктор перемещения определяется как удаленный,
если у класса есть член, определяющий собственный конструктор копий, но не определяющий конструктор перемещения, или если у класса есть член, который не определяет собственные функции копирования и для которого компилятор неспособен синтезировать конструктор перемещения. То же относится к присваиванию при перемещении.
• Конструктор перемещения и оператор присваивания при перемещении определяются как удаленные, если у класса есть член, собственный конструктор перемещения которого или оператор присваивания при перемещении которого удален или недоступен.
• Как и конструктор копий, конструктор перемещения определяется как удаленный, если деструктор удален или недоступен.
• Как и оператор присвоения копии, оператор присваивания при перемещении определяется как удаленный, если у класса есть константный или ссылочный член.
Предположим, например, что в классе 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

HasPtr& operator=(HasPtr rhs)
{ swap(*this, rhs); return *this; }
// другие члены как в p. 13.2.1
};
В этой версии класса добавлен конструктор перемещения, получающий значения из своего аргумента. Тело конструктора обнуляет указатель-член данного объекта класса HasPtr, чтобы гарантировать безопасное удаление оригинального объекта перемещения. Эта функция не делает ничего, она не может передать исключение, поэтому отметим ее как noexcept (см.
раздел 13.6.2).
Теперь рассмотрим оператор присвоения. У него есть не ссылочный параметр, а значит, этот параметр инициализируется копией (см. раздел 13.1.1). В зависимости от типа аргумента инициализация копией использует либо конструктор копий, либо конструктор перемещения;
l-значения копируются, а r-значения перемещаются. В результате этот оператор однократного присвоения действует и как присвоение копии, и как присваивание при перемещении.
Предположим, например, что объекты hp и hp2 являются объектами класса HasPtr: hp = hp2; // hp2 - l-значение; для копирования hp2 используется
// конструктор копий hp = std::move(hp2); // hp2 перемещает конструктор перемещения
В первом случае присвоения правый операнд — l-значение, поэтому конструктор перемещения не подходит. Для инициализации rhs будет использоваться конструктор копий.
Он будет резервировать новую строку и копировать ту строку, на которую указывает hp2.
Во втором случае присвоения вызывается функция std::move() для связывания ссылки на r-значение с объектом hp2. В данном случае подходят и конструктор копий, и конструктор перемещения. Но поскольку аргумент — это ссылка на r-значение, точное соответствие обеспечит конструктор перемещения. Конструктор перемещения копирует указатель из объекта hp2 и не резервирует память.
Независимо от того, использовался ли конструктор копии или перемещения, тело оператора присвоения обменивает содержимое двух своих операндов. Обмен объектов класса HasPtr приводит к обмену указателями-членами и переменными-членами (типа int) этих двух объектов. После вызова функции swap() правый операнд будет содержать указатель на строку, который ранее принадлежал левому. При выходе rhs из области видимости эта строка будет удалена. Совет. Обновленное правило трех
Все пять функций-членов управления копированием можно считать единым блоком: если класс определяет любую из этих функций, он должен обычно определять их все. Как уже упоминалось, для правильной работы некоторые классы
Page 677/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
удалить прежние данные левого операнда. В данном случае удаление левого операнда требует удаления указателей на это сообщение из существующих папок, что и делает вызов функции remove_from_Folders(). После удаления из папок происходит вызов функции move(),
чтобы переместить 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
elements = first; // обновить указатели first_free = last; cap = elements + newcapacity;
}
Алгоритм 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
public: void push_back(const std::string&); // копирует элемент void push_back(std::string&&); // перемещает элемент
// другие члены как прежде
};
// неизменно с оригинальной версии в разделе 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
string s = "some string or another"; vec.push_back(s); // вызов push_back(const string&) vec.push_back("done"); // вызов push_back(string&&)
Эти вызовы различаются тем, является ли аргумент 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

// сделать все необходимое для присвоения rhs этому объекту return *this;
}
Квалификаторы ссылки & или && означают, что указатель this может указывать на r- или l-значение соответственно. Подобно спецификатору const, квалификатор ссылки может быть применен только к (нестатической) функции-члену и должен присутствовать как в объявлении, так и в определении функции.
Функцию, квалифицированную символом &, можно применить только к l-значению, а функцию, квалифицированную символом &&,— только к r-значению:
Foo &retFoo(); // возвращает ссылку;
// вызов retFoo() является l-значением
Foo retVal(); // возвращает значение; вызов retVal() - r-значение
Foo i, j; // i и j - это l-значения i = j; // ok: i - это l-значение retFoo() = j; // ok: retFoo() возвращает l-значение retVal() = j; // ошибка: retVal() возвращает r-значение i = retVal(); // ok: вполне можно передать r-значение как правый
// операнд присвоения
Функция может быть квалифицирована и ссылкой, и константой. В таких случаях квалификатор ссылки должен следовать за спецификатором const: class Foo { public:
Page 685/1103

Foo someMem() & const; // ошибка: первым должен быть
// спецификатор const
Foo anotherMem() const &; // ok: спецификатор const расположен первым
}; Перегрузка и ссылочные функции
Подобно тому, как можно перегрузить функцию-член на основании константности параметра
(см. раздел 7.3.2), ее можно перегрузить на основании квалификатора ссылки. Кроме того,
функцию можно перегрузить на основании квалификатора ссылки и константности. В
качестве примера придадим классу Foo член типа vector и функцию sorted(), возвращающую копию объекта класса Foo, в котором сортируется вектор: class Foo { public:
Foo sorted() &&; // применимо к изменяемым r-значениям
Foo sorted() const &; // применимо к любому объекту класса Foo
// другие члены класса Foo private: vector<int> data;
};
// этот объект - r-значение, поэтому его можно сортировать на месте
Foo Foo::sorted() && { sort(data.begin(), data.end()); return *this;
}
// этот объект либо константа, либо l-значение;
//
Page 686/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

// он применим для сравнения целочисленных значений using Comp = bool(const int&, const int&);
Foo sorted(Comp*); // ok: другой список параметров
Foo sorted(Comp*) const; // ok: ни одна из версий не квалифицирована
// как ссылка
};
Здесь объявление константной версии функции sorted() без параметров является ошибкой.
Есть вторая версия функции sorted() без параметров, и у нее есть квалификатор ссылки,
поэтому у константной версии этой функции также должен быть квалификатор ссылки. С
другой стороны, те версии функции sorted(), которые получают указатель на функцию сравнения, прекрасно работают, поскольку ни у одной из функций нет спецификатора.
Если у функции-члена есть квалификатор ссылки, то у всех версий этой функции-члена с тем же списком параметров должны быть квалификаторы ссылки. Упражнения раздела 13.6.3
Упражнение 13.55. Добавьте в класс StrBlob функцию push_back() в версии ссылки на r-значение.
Упражнение 13.56. Что бы было при таком определении функции sorted():
Foo Foo::sorted() const & {
Foo ret(*this); return ret.sorted();
}
Упражнение 13.57. Что если бы функция sorted() была определена так:
Foo Foo::sorted() const & { return Foo(*this).sorted();
}
Упражнение 13.58. Напишите версию класса Foo с операторами вывода в функциях sorted(),
чтобы проверить свои ответы на два предыдущих упражнения.
Резюме
Page 688/1103

Каждый класс контролирует происходящее при копировании, перемещении, присвоении и удалении объектов его типа. Эти действия определяют специальные функции-члены:
конструктор копий, конструктор перемещения, оператор присвоения копии, оператор присваивания при перемещении и деструктор. Конструктор перемещения и оператор присваивания при перемещении (обычно неконстантный) получают ссылку на r-значение;
версии оператора копирования (обычно константные) получают обычную ссылку на l-значение.
Если класс не объявит ни одну из этих функций, то компилятор определит их автоматически.
Если они не определены как удаленные, эти функции-члены инициализирует, перемещают,
присваивают и удаляют объект, обрабатывая каждую нестатическую переменную-член по очереди. Синтезируемая функция делает то, что соответствует типу элемента для перемещения, копирования, присвоения и удаления этого элемента.
Классы, резервирующие память или другие ресурсы, почти всегда требуют, чтобы класс определил функции-члены управления копированием для управления зарезервированным ресурсом. Если класс нуждается в деструкторе, то он почти наверняка должен определить конструкторы перемещения и копирования, а также операторы перемещения и присвоения копии.
Термины
Деструктор (destructor). Специальная функция-член, освобождающая занятую объектом память, когда он выходит из области видимости или удаляется. Компилятор автоматически удаляет каждый член класса. При удалении переменных-членов типа класса используются их собственные деструкторы, а при удалении переменных-членов встроенного или составного типа конструктор ничего не делает. В частности, объект, на который указывает указатель-член класса, автоматически не удаляется деструктором.
Инициализация копией (copy initialization). Форма инициализации с использованием оператора = и предоставления инициализатора для создаваемого объекта. Используется также при передаче и возвращении объекта по значению, при инициализации массива или агрегатного класса. Инициализация копией использует конструктор копий или конструктор перемещения, в зависимости от того, является ли инициализатор l- или r-значением.
Итератор перемещения (move iterator). Адаптер, позволяющий создать итератор, обращение к значению которого возвращает ссылку на r-значение.
Квалификатор ссылки (reference qualifier). Символ, обычно указывающий, что нестатическая функция-член может быть вызвана для l- или r-значения. Спецификатор & или
&& следует за списком параметров или спецификатором const, если он есть.
Функция с квалификатором & может быть вызвана только для l-значений, а функция с квалификатором && — только для r-значений.
Конструктор копий (copy constructor). Конструктор, который инициализирует новый объект как копию другого объекта того же типа. При передаче объекта в функцию или из функции конструктор копий применяется неявно. Если конструктор копий не определен явно,
компилятор синтезирует его самостоятельно.
Конструктор перемещения (move constructor). Конструктор, получающий ссылку на r-значение своего типа. Как правило, конструктор перемещения перемещает данные своего параметра во вновь созданный объект. После перемещения запуск деструктора для правого операнда
Page 689/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
перемещения) устанавливают счетчик ссылок в 1. Каждый раз, когда создается новая копия,
значение счетчика увеличивается. Когда объект удаляется, значение счетчика уменьшается.
Оператор присвоения и деструктор проверяют, не достиг ли декремент счетчика ссылок нуля,
и если это так, то они удаляют объект.
Удаленная функция (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

14.1. Фундаментальные концепции
Перегруженный оператор — это функция со специальным именем, состоящим из ключевого слова operator, сопровождаемого символом определяемого оператора. Подобно любой другой функции, перегруженный оператор имеет тип возвращаемого значения и список параметров.
Количество параметров функции перегруженного оператора совпадает с количеством операндов оператора. У унарного оператора — один параметр; у бинарного — два. В
бинарном операторе левый операнд передается первому параметру, а правый операнд —
второму. За исключением перегруженного оператора вызова функции, operator(), у перегруженного оператора не может быть аргументов по умолчанию (см. раздел 6.5.1).
Если перегруженный оператор является функцией-членом, то первый (левый) операнд связывается с неявным указателем this (см. раздел 7.1.2). Поскольку первый операнд неявно связан с указателем this, функция оператора-члена будет иметь на один явный параметр меньше, чем операндов у оператора.
Когда перегруженный оператор является функцией-членом, указатель this соответствует левому операнду. У операторов-членов на один явный параметр меньше, чем операндов.
Функция оператора должна быть либо членом класса, либо иметь по крайней мере один параметр типа класса:
// ошибка: переопределить встроенный оператор для целых чисел int operator*(int, int);
Это ограничение означает невозможность изменить смысл оператора, относящегося к операндам встроенного типа.
Перегрузить можно многие, но не все операторы. Табл. 14.1 демонстрирует, может ли оператор быть перегружен. Перегрузка операторов new и delete рассматривается в разделе
19.1.1 .
Перегрузить можно только существующие операторы и нельзя изобрести новые символы операторов. Например, нельзя определить оператор operator** для возведения числа в степень.
Таблица 14.1. Операторы Операторы, которые могут быть перегружены + - * / % ^ & |

! ,
= < > <= >= ++ -- << >> == != && || += -= /= %= ^= &= |= *=
<<= >>= [] () -> ->* new new [] delete delete [] Операторы, которые не могут быть перегружены :: .* . ?:
Четыре символа (+, -, * и &) служат и унарными операторами, и бинарными. Перегружен может быть один или оба из этих операторов. Определяемый оператор задает количество параметров: x == y + z;
Page 692/1103

Это будет эквивалентно x == (y + z). Непосредственный вызов функции перегруженного оператора
Обычно функцию перегруженного оператора вызывают косвенно, применив оператор к аргументам соответствующего типа. Но функцию перегруженного оператора можно также вызвать непосредственно, как обычную функцию. Достаточно указать имя функции и передать соответствующее количество аргументов соответствующего типа:
// эквивалент вызова функции оператора, не являющегося членом класса data1 + data2; // обычное выражение operator+(data1, data2); // эквивалентный вызов функции
Эти вызовы эквивалентны: оба они являются вызовом функции не члена класса operator+() с передачей data1 как первого аргумента и data2, так и второго.
Явный вызов функции оператора-члена осуществляется таким же образом, как и вызов любой другой функции-члена: имя объекта (или указателя), для которого выполняется функция, и оператор точки (или стрелки) для выбора функции, которую следует вызвать: data1 += data2; // вызов на базе выражения data1.operator+=(data2); // эквивалентный вызов функции оператора-члена
Каждый из этих операторов вызывает функцию-член operator+=, где указатель this содержит адрес объекта data1, а объект data2 передан как аргумент. Некоторые операторы не следует перегружать
Помните, что некоторые операторы гарантируют порядок вычисления операндов. Поскольку использование перегруженного оператора на самом деле является вызовом функции, эти гарантии не распространяются на перегруженные операторы. В частности, гарантии вычисления операндов логических операторов AND и OR (см. раздел 4.3), оператора запятая
(см. раздел 4.10) не сохраняются. Кроме того, перегруженные версии операторов &&
и || не поддерживают вычислений по сокращенной схеме. Оба операнда вычисляются всегда.
Поскольку перегруженные версии этих операторов не сохраняют порядок вычисления и (или)
не поддерживают вычисления по сокращенной схеме, их перегрузка обычно — плохая идея.
Пользователи, вероятно, будут удивлены отсутствием привычных гарантий последовательности вычисления в коде при использовании перегруженной версии одного из этих операторов.
Еще один повод не перегружать операторы запятой и обращения к адресу заключается в том,
что, в отличие от большинства операторов, язык сам определяет значение этих операторов,
когда они применены к объектам типа класса. Поскольку у этих операторов есть встроенное значение, они обычно не должны перегружаться. Пользователи класса будут удивлены, если они поведут себя не так, как обычно.
Page 693/1103

Обычно операторы запятая, обращение к адресу, логический оператор AND и OR не должны быть перегружены.Использование определений, совместимых со встроенным смыслом
При разработке класса всегда следует сначала подумать об обеспечиваемых им операциях.
Только определившись с необходимыми операциями, следует подумать о том, стоит ли определить некую операцию как обычную функцию или как перегруженный оператор. Те операции, которые логически соответствуют операторам, — это хорошие кандидаты на определение в качестве перегруженных операторов.
• Если класс осуществляет операции ввода и вывода, имеет смысл определить операторы сдвига для совместимости с таковыми у встроенных типов.
• Если класс подразумевает проверку на равенство, определите оператор operator==. Если у класса есть оператор operator==, то у него обычно должен быть также оператор operator!=.
• Если у класса должна быть операция упорядочивания, определите оператор operator<
Если у класса есть оператор operator<, то у него, вероятно, должны быть все операторы сравнения.
• Тип возвращаемого значения перегруженного оператора обычно должен быть совместимым с таковым у встроенной версии оператора: логические операторы и операторы отношения должны возвращать значение типа bool, арифметические операторы должны возвращать значение типа класса, операторы присвоения и составные операторы присвоения должны возвращать ссылку на левый операнд. Составные операторы присвоения
Операторы присвоения должны вести себя аналогично синтезируемым операторам: после присвоения значения левых и правых операндов должны быть одинаковы, а возвратить оператор должен ссылку на левый операнд. Перегруженный оператор присвоения должен обобщить смысл встроенного оператора присвоения, а не переиначивать его. Внимание!
Будьте осторожны при использовании перегруженных операторов
Каждый оператор имеет некий смысл, когда он используется для встроенных типов.
Бинарный оператор +, например, всегда означает сумму. Вполне логично и удобно применять в классе бинарный оператор + для аналогичной функции. Например, библиотечный тип string,
в соответствии с соглашением, общепринятым для множества языков программирования,
использует оператор + для конкатенации, т.е. добавления содержимого одной строки в другую.
Перегруженные операторы полезней всего тогда, когда смысл встроенного оператора логически соответствует функции текущего класса. Применение перегруженных операторов вместо именованных функций позволяет сделать программы более простыми,
естественными и интуитивно понятными. Злоупотребление перегруженными операторами, а также придание не свойственного им смысла сделает класс неудобным в применении.
На практике вполне очевидные случаи противоестественной перегрузки операторов довольно редки. Например, ни один ответственный программист не переопределил бы оператор operator+ для вычитания. Зато очень часто предпринимаются попытки неким образом приспособить "обычный" оператор, который неприменим к данному классу. Операторы следует использовать только для тех функций, которые будут однозначно поняты пользователями. Оператор с неоднозначным смыслом, например равенство, может быть интерпретирован по-разному.
Если класс обладает арифметическим (см. раздел 4.2) или побитовым (см. раздел 4.8)
оператором, то его, как правило, имеет смысл снабдить соответствующими составными
Page 694/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

"hi".operator+(s). Однако литерал "hi" имеет тип const char*, т.е. встроенный тип; у него нет функций-членов.
Поскольку класс string определяет оператор + как обычную функцию, не являющуюся членом класса, сложение "hi" + s эквивалентно вызову operator+("hi", s). Подобно любому вызову функции, каждый из аргументов должен быть преобразуем в тип параметра. Единственное требование — по крайней мере один из операндов должен иметь тип класса, а оба операнда могут быть преобразованы в строку. Упражнения раздела 14.1

1   ...   33   34   35   36   37   38   39   40   ...   54


написать администратору сайта