Язык программирования C++. Пятое издание - Стенли Липпман
Шрифт:
Интервал:
Закладка:
// присвоение перемещения
В первом случае оператору присвоения передается объект 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).