Сайт о телевидении

Сайт о телевидении

» » Краткое введение в rvalue-ссылки. Перемещение, копирование и удаление файлов

Краткое введение в rvalue-ссылки. Перемещение, копирование и удаление файлов

Мне лично в языке C++ всегда казалась довольно сложной для понимания тема всех эти copy assignment’ов, move constructor’ов, perfect forwarding’а и вот этого всего. Поскольку без этих знаний в современном C++ далеко не уедешь, решил попробовать во всем разобраться. Не могу сказать, что теперь владею материалом в совершенстве, но на небольшую заметку-введение вроде наскреблось. Авось кому будет интересно.

Базовый код с запретом копирования и присваивания

Если вы пока точно не знаете, как будет использоваться класс, лучше всего просто запретить копирование и присваивание. По умолчанию они разрешены и просто копируют все атрибуты класса. Часто это не то, чего вы хотите. Например, тупо копировать какие-то указатели, файловые дескрипторы или мьютексы, являющиеся атрибутами класса, явно плохая идея. Простейший код, в котором копирование и присваивание класса явно запрещены:

#include

class Coord2D {
public :
Coord2D() {
_x = 0 ;
_y = 0 ;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") created" << std:: endl ;
}

Coord2D(int x, int y) {
_x = x;
_y = y;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") created" << std:: endl ;
}

~Coord2D() {
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") destroyed" << std:: endl ;
}

int getX() const { return _x; }
int getY() const { return _y; }
Coord2D& setX(int x) { _x = x; return * this ; }
Coord2D& setY(int y) { _y = y; return * this ; }

Coord2D(Coord2D const & ) = delete ;

private :
int _x, _y;
} ;

int main() {
Coord2D c1;
Coord2D c2(1 , 2 ) ;

Std:: cout << "Hi!" << std:: endl ;
}

Вывод программы:

Coord2D(x = 0, y = 0) created

Hi!

Coord2D(x = 0, y = 0) destroyed

Пока что никаких неожиданностей. Стоит отметить, что вместо:

Coord2D(Coord2D const & ) = delete ;
void operator= (Coord2D const & ) = delete ;

… можно написать:

Coord2D(Coord2D const & ) = default ;
void operator= (Coord2D const & ) = default ;

… тем самым явно указав на то, что вас устраивают реализации по умолчанию.

Copy constructor

Объявим copy contructor:

/* ... */

Coord2D(Coord2D const & obj) {
_x = obj._x;
_y = obj._y;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") copied" << std:: endl ;
}

/* ... */

int main() {
Coord2D c1(1 , 2 ) ;
Coord2D c2(c1) ;
Coord2D c3 = c1;
std:: cout << "Hi!" << std:: endl ;
}

Заметьте, что в нем мы имеем доступ к private полям второго экземпляра класса (obj), несмотря на то, что это другой экземпляр. Вывод программы:

Coord2D(x = 1, y = 2) created
Coord2D(x = 1, y = 2) copied
Coord2D(x = 1, y = 2) copied
Hi!
Coord2D(x = 1, y = 2) destroyed
Coord2D(x = 1, y = 2) destroyed
Coord2D(x = 1, y = 2) destroyed

Оба синтаксиса эквивалентны, в обоих случаях был вызван copy constructor. Конструктор для каждого объекта был вызван один раз. Можно было и не писать этот код, так как реализация copy constructor по умолчанию и так просто копирует атрибуты класса.

Copy assignment

Объявим copy assignment оператор:

/* ... */

void operator= (Coord2D const & obj) {
_x = obj._x;
_y = obj._y;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") copy-assigned" << std:: endl ;
}

/* ... */

int main() {
Coord2D c1(1 , 2 ) ;
Coord2D c2(c1) ;
Coord2D c3 = c1;

C2 = c3;

Std:: cout << "Hi!" << std:: endl ;
}

Coord2D(x = 1, y = 2) created
Coord2D(x = 1, y = 2) copied
Coord2D(x = 1, y = 2) copied
Coord2D(x = 1, y = 2) copy-assigned
Hi!
Coord2D(x = 1, y = 2) destroyed
Coord2D(x = 1, y = 2) destroyed
Coord2D(x = 1, y = 2) destroyed

Заметьте, что деструктор при присвоении не вызывается. Это означает, что в реализации copy assignment следует освобождать старые ресурсы перед присвоением новых значений.

Move constructor

Перепишем код следующим образом:

/* ... */

Coord2D id(Coord2D x) {
std:: cout << "id called" << std:: endl ;
return x;
}

int main() {
Coord2D c1 = id(Coord2D(1 ,2 ) ) ;
c1.setX (- 1 ) ;
std:: cout << "Hi!" << std:: endl ;
}

Coord2D(x = 1, y = 2) created
id called
Coord2D(x = 1, y = 2) copied
Coord2D(x = 1, y = 2) destroyed
Hi!

Как видите, мы создаем копию из временного объекта, после чего он сразу уничтожается. Для нас это не проблема, так как объект маленький. Но если бы он содержал в себе большие объемы данных, мы бы создали их полную копию, а затем одну из копий освободили бы. Для решения этой проблемы придумали move constructor:

/* ... */
Coord2D(Coord2D&& obj) {
_x = obj._x;
_y = obj._y;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") moved" << std:: endl ;
}
/* ... */

Coord2D(x = 1, y = 2) created
id called
Coord2D(x = 1, y = 2) moved
Coord2D(x = 1, y = 2) destroyed
Hi!
Coord2D(x = -1, y = 2) destroyed

Move constructor вызывается вместо copy constructor в случае, когда объект, из которого создается копия, вот-вот будет уничтожен. В таком конструкторе обычно данные из временного объекта переносятся в новый объект, а полям временного объекта присваиваются nullptr или что-то такое. Важно понимать, что при выходе из move constructor оба объекта должны оставаться валидными и для обоих должен корректно отрабатывать деструктор. Ссылка T&& называется rvalue reference и означает ссылку на объект, который вот-вот будет уничтожен.

Move assignment

Аналогично move constructor, только для присваивания. Например, код:

int main() {
Coord2D c1(1 ,2 ) ;
c1 = Coord2D(4 ,5 ) ;

Std:: cout << "Hi!" << std:: endl ;
}

… выведет:

Coord2D(x = 1, y = 2) created

Coord2D(x = 4, y = 5) copy-assigned

Hi!
Coord2D(x = 4, y = 5) destroyed

Объявим move assignment оператор:

/* ... */
void operator= (Coord2D&& obj) {
_x = obj._x;
_y = obj._y;
std:: cout << "Coord2D(x = " << _x << ", y = " << _y <<
") move-assigned" << std:: endl ;
}
/* ... */

Coord2D(x = 1, y = 2) created
Coord2D(x = 4, y = 5) created
Coord2D(x = 4, y = 5) move-assigned
Coord2D(x = 4, y = 5) destroyed
Hi!
Coord2D(x = 4, y = 5) destroyed

Move assignment оператор позволяет применить те же оптимизации, что и move constructor. В move constructor поля объекта, переданного в качестве аргумента, обычно как-то зануляются. В move assignment лучше сделать swap полей в двух объектах. Это позволит избавиться от дублирования кода между оператором move assignment и деструктором.

std::move

Move constructor бывает трудно стригерить. Например, код:

int main() {
Coord2D c1(Coord2D(1 ,2 ) .setX (5 ) ) ;
std:: cout << "Hi!" << std:: endl ;
}

… выведет:

Coord2D(x = 1, y = 2) created
Coord2D(x = 5, y = 2) copied

Hi!
Coord2D(x = 5, y = 2) destroyed

Так происходит, потому что метод setX возвращает lvalue reference, а у move constructor на входе совершенно другой тип, rvalue reference. Чтобы явно показать, что временный объект мы больше использовать не будем, предусмотрен std:move. Если переписать код так:

int main() {
Coord2D c1(std:: move (Coord2D(1 ,2 ) .setX (5 ) ) ) ;
std:: cout << "Hi!" << std:: endl ;
}

… программа выведет:

Coord2D(x = 1, y = 2) created
Coord2D(x = 5, y = 2) moved
Coord2D(x = 5, y = 2) destroyed
Hi!
Coord2D(x = 5, y = 2) destroyed

В сущности, std::move просто кастует lvalue reference (T&) в rvalue reference (T&&), больше ничего. При чтении кода std::move как бы говорит нам, что мы отдаем владение объектом в этом месте и далее не собираемся его использовать.

std::forward

Шаблон std::forward предназначен исключительно для написания шаблонных методов, способных принимать на вход как lvalue, так и rvalue, в зависимости от того, что передал пользователь, и передавать соответствующий тип далее без изменений. Техника получила название perfect forwarding.

Рассмотрим пример. Определим оператор сложения двух координат:

/* ... */

template < class T>
friend Coord2D operator+ (T&& a, const Coord2D& b) {
std:: cout << "Creating `Coord2D t`..." << std:: endl ;
Coord2D t(std:: forward < T> (a) ) ;
std:: cout << "`Coord2D t` created!" << std:: endl ;

return t.setX (t.getX () + b.getX () ) .setY (t.getY () + b.getY () ) ;
}

/* ... */

int main() {
Coord2D c1(1 ,1 ) , c2(1 ,2 ) , c3(1 ,3 ) ;
Coord2D c4 = c1 + c2 + c3;

Std:: cout << "Hi!" << std:: endl ;
}

Coord2D(x = 1, y = 1) created
Coord2D(x = 1, y = 2) created
Coord2D(x = 1, y = 3) created
Creating `Coord2D t`...
Coord2D(x = 1, y = 1) copied
`Coord2D t` created!
Coord2D(x = 2, y = 3) copied
Coord2D(x = 2, y = 3) destroyed
Creating `Coord2D t`...
Coord2D(x = 2, y = 3) moved
`Coord2D t` created!
[...]

Смотрите, что происходит. При первом вызове оператора сложения переменная t инициализируется при помощи copy constructor, так как c1 не является временным объектом. Однако при втором вызове первым аргументом передается временный объект c1 + c2 , и из него переменная t инициализируется уже при помощи move constructor. То есть, фактически std::forward позволил написать процедуру один раз, вместо того, чтобы писать две версии — одну, принимающую первым аргументом lvalue reference, и вторую, работающую с rvalue reference.

Заключение

Заметьте, что думать про всякие move semantics и perfect forwarding нужно только при работе с объектами, держащими в себе много данных, и только если вы часто копируете или присваиваете такие объекты. Это исключительно оптимизация, и без нее все будет совершенно корректно работать (более того, ничего этого не существовало до появления C++11). Пока профайлер не говорит вам , что вы во что-то такое не уперлись, возможно, не стоит заморачиваться. Помните также, что компилятор зачастую может избавляться от лишнего копирования объектов, см return value optimization (RVO) и copy elision .

С другой стороны, теорию понимать стоит независимо от того, упирается ваш код в копирование и перемещение объектов, или нет. Как минимум, move semantics и иже с ним может использоваться в чужом коде. В частности, он используется в STL, см например метод emplace_back класса std::vector или метод emplace класса std::map . Кроме того, понимание move semantics будет весьма нелишним при использовании

  • Перевод

Перевод статьи «A Brief Introduction to Rvalue References», Howard E. Hinnant, Bjarne Stroustrup, Bronek Kozicki.

Rvalue ссылки – маленькое техническое расширение языка C++. Они позволяют программистам избегать логически ненужного копирования и обеспечивать возможность идеальной передачи (perfect forwarding). Прежде всего они предназначены для использования в высоко производительных проектах и библиотеках.

Введение

Этот документ даёт первичное представление о новой функции языка C++ – rvalue ссылке. Это краткое учебное руководство, а не полная статья. Для получения дополнительной информации посмотрите список ссылок в конце.

Rvalue ссылка

Rvalue ссылка – это составной тип, очень похожий на традиционную ссылку в C++. Чтобы различать эти два типа, мы будем называть традиционную C++ ссылку lvalue ссылка. Когда будет встречаться термин ссылка, то это относится к обоим видам ссылок, и к lvalue ссылкам, и к rvalue ссылкам.

A a; A&& a_ref2 = a; // это rvalue ссылка
Rvalue ссылка ведет себя точно так же, как и lvalue ссылка, за исключением того, что она может быть связана с временным объектом, тогда как lvalue связать с временным (не константным) объектом нельзя.

A& a_ref3 = A(); // Ошибка! A&& a_ref4 = A(); // Ok
Вопрос: С чего бы это могло нам потребоваться?!

Оказывается, что комбинация rvalue ссылок и lvalue ссылок - это то, что необходимо для лёгкой реализации семантики перемещения (move semantics). Rvalue ссылка может также использоваться для достижения идеальной передачи (perfect forwarding), что ранее было нерешенной проблемой в C++. Для большинства программистов rvalue ссылки позволяют создать более производительные библиотеки.

Семантика перемещений (move semantics)

Устранение побочных копий
Копирование может быть дорогим удовольствием. К примеру, для двух векторов, когда мы пишем v2 = v1 , то обычно это вызывает вызов функции, выделение памяти и цикл. Это, конечно, приемлемо, когда нам действительно нужны две копии вектора, но во многих случаях это не так: мы часто копируем вектор из одного места в другое, а потом удаляем старую копию. Рассмотрим:

Template swap(T& a, T& b) { T tmp(a); // сейчас мы имеем две копии объекта a a = b; // теперь у нас есть две копии объекта b b = tmp; // а теперь у нас две копии объекта tmp (т.е. a) }
В действительности нам не нужны копии a или b , мы просто хотели обменять их. Давайте попробуем еще раз:

Template swap(T& a, T& b) { T tmp(std::move(a)); a = std::move(b); b = std::move(tmp); }
Этот вызов move() возвращает значение объекта, переданного в качестве параметра, но не гарантирует сохранность этого объекта. К примеру, если в качестве параметра в move() передать vector , то можно обоснованно ожидать, что после работы функции от параметра останется вектор нулевой длины, так как все элементы будут перемещены, а не скопированы. Другими словами, перемещение – это считывание со стиранием (destructive read).

В данном случае мы оптимизировали swap специализацией. Однако мы не можем специализировать каждую функцию, которая копирует большой объект непосредственно перед тем, как она удаляет или перезаписывает его. Это было бы неконструктивно.

Главная задача rvalue ссылок состоит в том, чтобы позволить нам реализовывать перемещение без переписывания кода и издержек времени выполнения (runtime overhead).

Move
Функция move в действительности выполняет весьма скромную работу. Её задача состоит в том, чтобы принять либо lvalue, либо rvalue параметр, и вернуть его как rvalue без вызова конструктора копирования:

Template typename remove_reference::type&& move(T&& a) { return a; }
Теперь всё зависит от клиентского кода, где должны быть перегружены ключевые функции (например, конструктор копирования и оператор присваивания), определяющие будет ли параметр lvalue или rvalue. Если параметр lvalue, то необходимо выполнить копирование. Если rvalue, то можно безопасно выполнить перемещение.

Перегрузка для lvalue/rvalue
Рассмотрим простой класс, который владеет ресурсом и также обеспечивает семантику копирования (конструктор копирования и оператор присваивания). Например, clone_ptr мог бы владеть указателем и вызвать у него дорогой метод clone() для копирования:

Template class clone_ptr { private: T* ptr; public: // Конструктор explicit clone_ptr(T* p = 0) : ptr(p) {} // Деструктор ~clone_ptr() {delete ptr;} // Семантика копирования clone_ptr(const clone_ptr& p) : ptr(p.ptr ? p.ptr->clone() : 0) {} clone_ptr& operator=(const clone_ptr& p) { if (this != &p) { delete ptr; ptr = p.ptr ? p.ptr->clone() : 0; } return *this; } // Семантика перемещения clone_ptr(clone_ptr&& p) : ptr(p.ptr) {p.ptr = 0;} clone_ptr& operator=(clone_ptr&& p) { std::swap(ptr, p.ptr); return *this; } // Прочие операции T& operator*() const {return *ptr;} // ... };
За исключением семантики перемещения, clone_ptr – это код, который можно найти в сегодняшних книгах по C++. Пользователи могли бы использовать clone_ptr так:

Clone_ptr p2 = p1; // и p2 и p1 владеют каждый своим собственным указателем

Обратите внимание, что выполнение конструктора копирования или оператора присвоения для clone_ptr являются относительно дорогой операцией. Однако, когда источник копии является rvalue, можно избежать вызова потенциально дорогой операции clone() , воруя указатель rvalue (никто не заметит!). В семантике перемещения конструктор перемещения оставляет значение rvalue в создаваемом объекте, а оператор присваивания меняет местами значения текущего объекта с объектом rvalue ссылки.

Теперь, когда код пытается скопировать rvalue clone_ptr , или если есть явное разрешение считать источник копии rvalue (используя std::move), работа выполнится намного быстрее.

Clone_ptr p1(new derived); // ... clone_ptr p2 = std::move(p1); // теперь p2 владеет ссылкой, вместо p1
Для классов, составленных из других классов (или через включение, или через наследование), конструктор перемещения и перемещающее присвоение может легко быть реализовано при использовании функции std::move .

Class Derived: public Base { std::vector vec; std::string name; // ... public: // ... // Семантика перемещения Derived(Derived&& x) // объявлен как rvalue: Base(std::move(x)), vec(std::move(x.vec)), name(std::move(x.name)) { } Derived& operator=(Derived&& x) // объявлен как rvalue { Base::operator=(std::move(x)); vec = std::move(x.vec); name = std::move(x.name); return *this; } // ... };
Каждый подобъект будет теперь обработан как rvalue в конструкторе перемещения и операторе перемещающего присваивания объекта. У std::vector и std::string операции перемещения уже реализованы (точно так же, как и у нашего clone_ptr), которые позволяют избежать значительно более дорогих операций копирования.

Стоит отметить, что параметр x обработан как lvalue в операциях перемещения, несмотря на то, что он объявлен как rvalue ссылка. Поэтому необходимо использовать move(x) вместо просто x при передаче базовому классу. Это ключевой механизм безопасности семантики перемещения, разработанной для предотвращения случайной попытки двойного перемещения из некоторой именованной переменной. Все перемещения происходят только из rvalues или с явным приведением к rvalue (при помощи std::move). Если у переменной есть имя, то это lvalue.

Вопрос: А как насчет типов, которые не владеют ресурсами? (Например, std::complex ?)

В этом случае не требуется проводить никакой работы. Конструктор копирования уже оптимален для копирования с rvalue.

Перемещаемые, но не копируемые типы

К некоторым типам семантика копирования не применима, но их можно перемещать. Например:
  • fstream
  • unique_ptr (не разделяемое и не копируемое владение)
  • Тип, представляющий поток выполнения
Если такие типы делать перемещаемыми (хотя они остаются не копируемыми), то удобство их использования чрезвычайно увеличивается. Перемещаемый, но не копируемый объект может быть возвращен по значению из фабричного метода (паттерн):

Ifstream find_and_open_data_file(/* ... */); ... ifstream data_file = find_and_open_data_file(/* ... */); // Никаких копий!
В этом примере базовый дескриптор файла передан из одного объекта в другой, т.к. источник ifstream является rvalue. В любом момент времени есть только один дескриптор файла, и только один ifstream владеет им.

Перемещаемый, но не копируемый тип также может быть помещён в стандартные контейнеры. Если контейнеру необходимо “скопировать” элемент внутри себя (например, при реалокации vector), он просто переместит его вместо копирования.

Vector> v1, v2; v1.push_back(unique_ptr(new derived())); // OK, перемещение без копирования... v2 = v1; // Ошибка времени компиляции! Это не копируемый тип. v2 = move(v1); // Нормальное перемещение. Владение указателем будет передано v2.
Многие стандартные алгоритмы извлекают выгоду от перемещения элементов последовательности вместо их копирования. Это не только обеспечивает лучшую производительность (как в случае std::swap , реализация которого описала выше), но и позволяет этим алгоритмам работать с некопируемыми (но перемещаемыми) типами. Например, следующий код сортирует vector, основываясь на типе, который хранится в умном указателе:

Struct indirect_less { template bool operator()(const T& x, const T& y) {return *x < *y;} }; ... std::vector> v; ... std::sort(v.begin(), v.end(), indirect_less());
Поскольку алгоритм сортировки перемещает объекты unique_ptr , он будет использовать swap (который больше не требует поддержки копируемости от объектов, значения которых он обменивает) или конструктор перемещения / оператор перемещающего присваивания. Таким образом, на протяжении всей работы алгоритма поддерживается инвариант, по которому каждый хранимый объект находится во владении только одного умного указателя. Если бы алгоритм предпринял попытку копирования (к примеру, по ошибке программиста), то результатом была бы ошибка времени компиляции.

Идеальная передача (perfect forwarding)

Рассмотрим универсальный фабричный метод, который возвращает std::shared_ptr для только что созданного универсального типа. Такие фабричные методы ценны для инкапсуляции и локализации выделения ресурсов. Очевидно, фабричный метод должен принимать точно такой же набор параметров, что и конструктор типа создаваемого объекта. Сейчас это может быть реализовано так:

Template std::shared_ptr factory() // версия без аргументов { return std::shared_ptr(new T); } template std::shared_ptr factory(const A1& a1) // версия с одним аргументом { return std::shared_ptr(new T(a1)); } // все остальные версии
В интересах краткости мы будем фокусироваться на простой версии с одним параметром. Например:

Std::shared_ptr p = factory(5);
Вопрос: Что будет, если конструктор T получает параметр по не константной ссылке?

В этом случае мы получаем ошибку времени компиляции, поскольку константный параметр функции factory не будет связываться с неконстантным параметром конструктора типа T .

Для решения этой проблемы можно использовать неконстантный параметр в функции factory:

Template std::shared_ptr factory(A1& a1) { return std::shared_ptr(new T(a1)); }
Так намного лучше. Если тип с модификатором const будет передан factory , то константа будет выведена в шаблонный параметр (например, A1) и затем должным образом передана конструктору T . Точно так же, если фабрике будет передан неконстантный параметр, то он будет правильно передан конструктору T как неконстанта. В действительности именно так чаще всего реализуется передача параметра (например, std::bind).

Теперь рассмотрим следующую ситуацию:

Std::shared_ptr p = factory(5); // Ошибка! A* q = new A(5); // OK
Этот пример работал с первой версией factory , но теперь аргумент "5" вызывает шаблон factory , который будет выведен как int& и впоследствии не сможет быть связанным с rvalue "5". Таким образом, ни одно решение нельзя считать правильным, каждый страдает своими проблемами.

Вопрос: А может сделать перегрузку для каждой комбинации AI & и const AI &?

Это позволило бы нам обрабатывать все примеры, но приведёт к экспоненциальной стоимости: для нашего случая с двумя параметрами это потребовало бы 4 перегрузки. Для фабрики с тремя параметрами мы нуждались бы в 8 дополнительных перегрузках. Для фабрики с четырьмя параметрами потребовалось бы уже 16 перегрузок и т.д. Это совершенно не масштабируемое решение.

Template std::shared_ptr factory(A1&& a1) { return std::shared_ptr(new T(std::forward(a1))); }
Теперь rvalue параметры могут быть связаны с параметрами factory . Если параметр const , то он будет выведен в шаблонный тип параметра factory .

Вопрос: Что за функция forward используется в этом решении?

Как и move , forward - это простая стандартная библиотечная функция, используемая, чтобы выразить намерение непосредственно и явно, а не посредством потенциально загадочного использования ссылок. Мы хотим передать параметр a1 , и просто заявляем об этом.

Здесь, forward сохраняет lvalue/rvalue параметр, который был передан factory . Если factory был передан rvalue, то при помощи forward и конструктору T будет передан rvalue. Точно так же, если lvalue параметр передан factory , он же будет передан конструктору T как lvalue.

Определение функции forward может выглядеть примерно так:

Template struct identity { typedef T type; }; template T&& forward(typename identity::type&& a) { return a; }

Ссылки

Поскольку одна из основных целей этой заметки краткость, некоторые детали были сознательно опущены. Тем не менее, здесь покрыто 95% знаний по этой теме.

Для получения дальнейшей информации по семантики перемещения, такой как тесты производительности, детали перемещений, не копируемые типы, и многое другое смотрите

За счет которых возможно реализовать отдельные функции для обработки обычных (живущих дольше выражения) и временных объектов. Такой подход позволяет написать более оптимальный код, т.к. зная, что аргументом является временный объект (который все равно будет уничтожен после обработки), мы можем использовать данные этого объекта, вместо создания новых. Такое поведение называется семантикой перемещения, для его реализации служат перемещающие конструкторы, принимающие на вход rvalue ссылку. Расскажите про них с примерами.

  • Перемещающий конструктор. Примеры

    Рассмотрим функцию, выполняющую переворот строки, учитывая, что на вход подано rvalue (временный объект) :

    String reverse(string&& str) { string reverse_string(str.buffer); str.buffer = nullptr; std::reverse(reverse_string.begin(), reverse_string.end()); return reverse_string; }

    Тут экономия (более высокая эффективность) обеспечивается за счет того, что результирующая строка использует буфер (какие-то сырые данные, возможно char*) из временной строки. Более подробно про то, как работает этот код: . Проблема тут заключается в нарушении инкапсуляции, т.к. сторонний код (функция реверса строки) не должна иметь доступ к сырым данным строки (и даже знать как она устроена внутри).

    Решением проблемы являются перемещающие конструкторы, которые вызываются автоматически вместо конструкторов копирования если аргументом является временный объект. Для сравнения рассмотрим конструктор копирования строки и перемещающий конструктор:

    String(string const& str) : m_buf(new char), m_size(str.size()) { strcpy(m_buf, str.m_buf); } string(string const&& str) : m_buf(str.m_buf), // char* m_buf - заменяется указатель m_size(str.size()) { str.m_buf = nullptr; str.m_size = 0; }

    Конструктор копирования, как это обычно происходит, создает новый буфер для хранения данных, вызывая оператор new , а перемещающий конструктор — забирает данные у переданного ему временного объекта. Мы можем реализовать такой конструктор и для нашего (класса массива, размер которого увеличивается автоматически при вставке элемента):

    Template Array::Array(Array&& array) : m_size(array.m_size), m_realSize(array.m_realSize), m_array(array.m_array) { array.m_size = 0; array.m_realSize = 0; array.m_array = nullptr; }

    В этих примерах перемещающий конструктор является лишь более оптимальной версией конструктора копирования для случаев, когда на вход подан временный объект, например:

    Void foo(string str); void bar(const string& str); foo(employee.get_name()); bar(employee.get_office());

    Преимущества перемещающего конструктора

    Было показано, что перемещающий конструктор является облегченной версией конструктора копирования , однако иногда реализуют перемещающий конструктор для классов, копирование в которых запрещено. Ярчайший пример — std::unique_ptr<> , представляющий собой один из умных указателей стандартной библиотеки, реализующий . Суть этого класса заключается в том, что он владеет единственным указателем на некоторый объект (и автоматически разрушает объект в определенных ситуациях). Указатель должен быть единственным, следовательно конструктор копирования не должен быть доступен, однако перемещение для этого класса вполне логично (мы можем передавать владение указателем от одного объекта к другому — поэтому unique_ptr , в частности, может быть использован в качестве возвращаемого значения функции. Другими примерами такого поведения из стандартной библиотеки являются классы std::fstream и std::thread — копирование для них лишено смысла, однако передача владения файлом из одной функции в другую может быть логична.
    Таким образом, семантика перемещения является не только средством повышения эффективности программ, но и позволяет реализовать передачу владения объектом в случаях, когда копирование запрещено (является очень длительной операцией или вообще лишено смысла).

    Стоит отметить, что не для всех классов имеет смысл писать перемещающий конструктор. Так, вы не сможете например повысить эффективность за счет перемещения класса точки или любого другого класса не владеющего внешним ресурсом. Ведь повышение эффективности обеспечивается заменой копирования этого ресурса перемещением:

    Struct Point { double x, y; Point(const Point& point) : x(point.x), y(point.y) { } }

    Для этого примера не получится реализовать перемещающий конструктор более эффективно, чем конструктор копирования.

    std::move

    В ряде случаев нужно явно указать компилятору, что объект является временным — сделать это можно с помощью функции std::move , выполняющей приведение типа к rvalue (эквивалентной static_cast). Если вам нужно переместить именованный (не временный) объект — используйте std::move , т.к. в противном случае будет вызван конструктор копирования:

    Void foo(Type&& obj) { Type copy_obj(obj); // вызов конструктора копирования Type move_obj(std::move(obj)); // вызов перемещающего конструктора } foo(Type()); // успешный вызов функции foo Type obj; foo(obj); // ошибка, функция принимает ссылку на r-значение, но мы передаем l-значение

    Этот пример напоминает, что функции, принимающие указатели на rvalue будут вызваны только для временных объектов. Однако, внутри функции эти объекты уже не являются временными, ведь у них есть имя — поэтому при создании объекта copy_obj будет вызван конструктор копирования. Если нам нужно перемещение — нужно явно указать, что объект является временным с помощью std::move .

    В рассмотренном выше примере данные строки (класс string) хранились в виде массива символов, поэтому вызов m_buf(str.m_buf) работал эффективно — просто заменялся указатель. Однако, если данных хранились бы в векторе, то такой код привел бы к вызову конструктора копирования:

    String(string const&& str) : m_buf(str.m_buf) // vector m_buf, вызывается конструктор копирования { }
    Решить проблему можно с помощью std::move:
    string(string const&& str) : m_buf(std::move(str.m_buf)) // vector m_buf, вызывается перемещающий конструктор { }

    Важно помнить, что std::move не перемещает объект, а лишь выполняет приведение типа, которое позволяет вызвать перемещающий конструктор.

    std::swap

    Функция std::swap предназначена для обмена значений двух объектов. До принятия стандарта С++11 обмен происходил с использованием вспомогательной переменной, что требовало выполнения конструктора копирования и двух операций присваивания:

    Template void swap(T& a, T& b) { T c(a); a=b; b=c; }

    Сейчас для классов, поддерживающих семантику перемещения эта функция вызывает перемещающий конструктор и два перемещающих оператора присваивания:

    Template void swap(T& a, T& b) { T c(std::move(a)); a=std::move(b); b=std::move(c); }

    Это работает очень эффективно и может применяться для более простой реализации перемещающего конструктора:

    String(string&& str) : string() { swap(*this, str); }

    Перемещающий конструктор вызывает конструктор по умолчанию, в результате чего создается строка нулевой длины (память не выделяется, код работает очень быстро). Затем текущий объект обменивается при помощи перемещающей функции swap с временным, переданным в качестве аргумента.
    Не во всех случаях такой подход будет одинаково эффективным, там для класса Array , рассмотренного выше, конструктор по умолчанию динамически выделяет блок памяти некоторого начального размера.

    Перемещающий оператор присваивания и std::swap

    Оператор присваивания является одной из наиболее часто используемых функций, даже std::swap использует его, при этом семантика перемещения может повысить эффективность:

    Array get_values(); Array values; values = get_values();
    В данном примере вызов функции создает временный объект, который присваивается объекту values . Если для класса Array не реализована перемещающая версия оператора, то произойдет копирование данных массива.
    template Array& operator=(Array&& source) { if (this != &source) { delete m_array; m_array = source.m_array; m_size = source.m_size; m_realSize = source.m_realSize; source.m_array = nullptr; } return *this; }

    Оператор выполняет проверку самоприсваивания чтобы не уничтожить единственную копию данных, затем освобождает память из под данных, уже имеющихся в объекте, помещает в него данные из объекта правой части оператор и обнуляет указатель временного объекта.



  •