13.6.2. Конструктор перемещения и присваивание при перемещении
Подобно классу string (и другим библиотечным классам), наши собственные классы могут извлечь пользу из способности перемещения ресурсов вместо копирования. Чтобы позволить собственным типам операции перемещения, следует определить конструктор перемещения и оператор присваивания при перемещении. Эти члены подобны соответствующим функциям копирования, но они захватывают ресурсы заданного объекта, а не копируют их.
Как и у конструктора копий, у конструктора перемещения есть начальный параметр, являющийся ссылкой на тип класса. В отличие от конструктора копии, ссылочный параметр конструктора перемещения является ссылкой на r-значение. Подобно конструктору копий, у всех дополнительных параметров должны быть аргументы по умолчанию.
Кроме перемещения ресурсов, конструктор перемещения должен гарантировать такое состояние перемещенного объекта, при котором его удаление будет безопасно. В частности, сразу после перемещения ресурса оригинальный объект больше не должен указывать на перемещенный ресурс, ответственность за него принимает вновь созданный объект.
В качестве примера определим конструктор перемещения для класса 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 указывают после списка параметров функции. В конструкторе его располагают между списком параметров и символом :, начинающим список инициализации конструктора:
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 должен использовать во время пересоздания конструктор копий вместо конструктора перемещения, если только не известно, что конструктор перемещения типа элемента не может передать исключение. Если необходимо, чтобы объекты типа были перемещены, а не скопированы при таких обстоятельствах, как пересоздание вектора, то следует явно указать библиотеке, что использовать конструктор перемещения безопасно. Для этого конструктор перемещения (и оператора присваивания при перемещении) следует отметить как 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(). Подобно любому другому оператору присвоения, крайне важно не освобождать ресурсы левого операнда прежде, чем использовать (возможно, те же) ресурсы правого операнда.
Исходный объект перемещения должен быть в удаляемом состоянии
Перемещение объекта не удаляет его оригинал: иногда после завершения операции перемещения оригинальный объект следует удалить. Поэтому, создавая функцию перемещения, следует гарантировать, что после перемещения оригинальный объект будет находиться в состоянии, допускающем запуск деструктора. Функция перемещения класса StrVec выполняет это требование и присваивает указателям-членам оригинального объекта значение nullptr.
Кроме гарантии безопасного удаления оригинального объекта, функции перемещения должны оставлять объект в допустимом состоянии. Обычно допустимым считается тот объект, которому может быть безопасно присвоено новое значение или который может быть использован другими способами, не зависящими от его текущего значения. С другой стороны, у функций перемещения нет никаких требований относительно значения, которое остается в оригинальном объекте. Таким образом, программы никогда не должны зависеть от значения оригинального объекта после перемещения.
Например, при перемещении объекта библиотечного класса string или контейнера известно, что оригинальный объект перемещения остается допустимым. В результате для оригинальных объектов перемещения можно выполнять такие функции, как empty() или size(). Однако предсказать результат их выполнения затруднительно. Логично было бы ожидать, что оригинальный объект перемещения будет пуст, но это не гарантируется.
Функции перемещения класса StrVec оставляют оригинальный объект перемещения в том же состоянии, в котором он находился бы после инициализации по умолчанию. Поэтому все функции класса StrVec продолжат выполняться с его объектом точно так же, как с любым другим инициализированным по умолчанию объектом класса StrVec. Другие классы, с более сложной внутренней структурой, могут вести себя по-другому.
После операции перемещения "оригинальный объект" должен остаться корректным, допускающим удаление объектом, но для пользователей его значение непредсказуемо.
Синтезируемые функции перемещения
Подобно конструктору копий и оператору присвоения копии, компилятор способен сам синтезировать конструктор перемещения и оператор присваивания при перемещении. Однако условия, при которых он синтезирует функции перемещения, весьма отличаются от тех, при которых он синтезирует функции копирования.
Помните, что если не объявить собственный конструктор копий или оператор присвоения копии, компилятор всегда синтезирует их сам (см. раздел 13.1.1 и раздел 13.1.2). Функции копирования определяются или как функции почленного копирования либо присвоения объекта, или как удаленные функции.
В отличие от функций копирования, для некоторых классов компилятор не синтезирует функции перемещения вообще. В частности, если класс определяет собственный конструктор копий, оператор присвоения копии или деструктор, конструктор перемещения и оператор присваивания при перемещении не синтезируются. В результате у некоторых классов нет конструктора перемещения или оператора присваивания при перемещении. Как будет продемонстрировано вскоре, когда у класса нет функции перемещения, вместо него в результате обычного подбора функции будет использована соответствующая функция копирования.
Компилятор синтезирует конструктор перемещения или оператор присваивания при перемещении, только если класс не определяет ни одной из собственных функций-членов управления копированием и если каждая нестатическая переменная-член класса может быть перемещена. Компилятор может перемещать члены встроенного типа, а также члены типа класса, если у него есть соответствующая функция-член перемещения:
// компилятор синтезирует функции перемещения для 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), но компилятор окажется неспособен переместить все члены, то функция перемещения будет определена как удаленная. Важное исключение из правила, согласно которому синтезируемая функция перемещения определяется как удаленная, подобно таковому для функций копирования (см. раздел 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 отсутствовало объявление конструктора перемещения, то компилятор не синтезировал бы конструктор перемещения вообще. Функции перемещения не синтезируются, если в противном случае они были определены как удаленные.
И последнее взаимоотношение между функциями перемещения и синтезируемыми функциями-членами управления копированием: тот факт, определяет ли класс собственные функции перемещения, влияет на то, как синтезируются функции копирования. Если класс определит любой конструктор перемещения и (или) оператор присваивания при перемещении, то синтезируемый конструктор копий и оператор присвоения копии для этого класса будут определены как удаленные.
Классы, определяющие конструктор перемещения или оператор присваивания при перемещении, должны также определять собственные функции копирования. В противном случае эти функции-члены по умолчанию удаляются.
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&); // конструктор копий
// другие члены, но 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;}
// оператор присвоения - и оператор перемещения, и присвоения копии
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 из области видимости эта строка будет удалена.
Совет. Обновленное правило трех
Все пять функций-членов управления копированием можно считать единым блоком: если класс определяет любую из этих функций, он должен обычно определять их все. Как уже упоминалось, для правильной работы некоторые классы должны определять конструктор копий, оператор присвоения копии и деструктор (см. раздел 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(), код все равно будет работать, но осуществляя ненужное копирование. Затем функция перебирает папки, удаляя указатель на оригинал сообщения и добавляя указатель на новое сообщение.
Следует заметить, что вставка элемента в набор может привести к передаче исключения, поскольку добавление элемента на контейнер требует резервирования памяти, вполне может быть передано исключение 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;
}
Подобно любым операторам присвоения, оператор присваивания при перемещении должен удалить прежние данные левого операнда. В данном случае удаление левого операнда требует удаления указателей на это сообщение из существующих папок, что и делает вызов функции 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(); // освободить прежнее пространство
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 оператор присвоения копии и присваивания при перемещении и сравните действия, выполняемые в новом операторе присваивания при перемещении, с версией копии и обмена.
Упражнение 13.54. Что бы случилось, если бы мы определи оператор присваивания при перемещении для класса HasPtr, но не изменили оператор копии и обмена? Напишите код для проверки вашего ответа.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОК