Перемещающий конструктор и семантика перемещения

  • Перевод

Перевод статьи «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% знаний по этой теме.

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

  • Перевод

Перевод статьи «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% знаний по этой теме.

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

Прежде чем говорить о новшествах, необходимо раскрыть тему свойств выражений . Итак, начнем, - lvalue и rvalue существуют достаточно давно, но не каждый С++ программист подозревает об их существовании и еще меньшая часть из них сможет с ходу определить какое выражение относиться к rvalue, а какое к lvalue. Необходимо знать, что lvalue и rvalue это свойство выражения, некоторые ошибочно полагают, что переменные или объекты имеют свойства rvalue\lvalue, но это предположение ложно т.к. только выражения обладают подобными свойствами.

Для понимания сути lvalue\rvalue можно заглянуть в историю появления их непрезентабельных имен: свои имена они получили благодаря фразам: right value(rvalue), т.е. выражение находящиеся справа и left value(lvalue), т.е. выражение находящееся слева. Это очень грубое объяснение сути rvalue\lvalue, но зато, их имена прекрасно отражают суть изначальной задумки комитета стандартизации. Со времени появления lvalue\rvalue много воды утекло, и сейчас их уже нельзя разделить на категории находящихся справа и слева, ведь для того, чтобы определить их местоположения, надо знать, относительно чего определять это самое местоположение. И тут нет никакого общего правила, ведь, например, слева от оператора "."(точка) может быть как lvalue, rvalue выражение, а следовательно нельзя говорить, что свойство выражения определяется его пространственным расположением.

Ну да ладно, хватит уроков истории пора перейти к практике. Простейшим способом определения является попытка получения адреса выражения; если Вы можете получить его адрес и в дальнейшем его использовать, тогда перед вами lvalue. Если же адрес выражения не может быть получен - перед вами rvalue. Кто-то может поспорить, что адрес все-таки можно получить, и некоторые компиляторы, возможно, дают это сделать. Тем не менее, это является нарушением стандарта C++ а именно: пункта 5.3.1/3. Да и если рассуждать логически: этот адрес не будет иметь никакого смысла, так как это адрес памяти, которую вы не контролируете и она может быть легко перезаписана в течение работы программы. Адрес же lvalue, это адрес постоянного хранилища, которое остается под контролем программиста на протяжении всей области жизни объекта.

Пример:

Int a = 0, b = 0; /* Можем ли мы получить адрес выражения (a + b) и использовать его в дальнейшем? Нет. Т.к. результатом сложения будет временный объект, доступ к которому не может быть получен за пределами строки выполнения оканчивающейся ";" (точкой с запятой). */ (a + b); /* Мы можем получить адрес результата этого выражения использовать его в дальнейшем т.к. адресом этого выражения будет служить адрес переменной a. */ a += b;

Еще один, более развернутый, пример:

Int foo(); int& bar(); int main() { int i = 0; &++i;//Ok, lvalue &i++;//error C2102: "&" requires l-value(VC++ 2010) &foo();//error C2102: "&" requires l-value(VC++ 2010) &bar();//Ok, lvalue } int foo() { return 0; } int& bar() { static int value = 0; return value; }

Хотя приведенное выше методика распознавания rvalue не универсальна, я считаю, что она может стать хорошим подспорьем для людей не искушенных в тонкостях С++ и позволит выработать ре��лекс на rvalue. Однако, хотелось бы предостеречь читателя от искушения считать временный объект и rvalue синонимами, еще раз напомню rvalue\lvalue это свойство выражения , а не объекта. Например:

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

Const int& lvalue = foo();

Объект возвращаемый foo() все еще является временным, но, согласно пункту 12.2/5 стандарта С++, время жизни временного объекта продлевается и становится таким же, как и время жизни ссылки, которая указывает на этот объект. Выражение же, в свою очередь, приобрело свойство lvalue. Таким образом, пример приведенный выше показывает, что нельзя ставить знак равенства между временным объектом и rvalue.

Пожалуй мы разобрались с lvalue\rvalue которые существуют в нынешнем стандарте С++03. Пора перейти к тому, что же предлагает по этому поводу новый стандарт. А предлагает он следующее разделение:

Новый стандарт вводит новые понятия, и расширяет идею rvalue\lvalue. Что же значат эти новые понятия? Давайте разберемся с каждым по очереди:

lvalue - здесь никаких изменений, lvalue свойство обозначает тоже самое, что обозначает в C++03.

xvalue (от английского " eXpiring "(находящийся на грани уничтожения)) - это свойство означает, что объект(результат выражения) находится в конце своего жизненного цикла, но еще не удален. xvalue появляется в тех выражениях, результат которых связан с rvalue ссылками (о них мы поговорим позже)

prvalue (pure (англ. чистый) " rvalue ") - в новом стандарте так обозвали rvalue из С++03.

rvalue - это свойство которое делится на xvalue и prvalue

glvalue - это свойство которое делится на xvalue и lvalue

Таким образом, в грядущем стандарте систему свойств выражений несколько усложнили. Хотя, если вы усвоили rvalue\lvalue свойства из С++03 то особых проблем с новыми свойствами у вас возникнуть не должно. Ведь lvalue и prvalue вам уже знакомы, а xvalue выходит на сцену лишь, когда в результате выражения фигурируют rvalue ссылки. Давайте поговорим об этих самых ссылках.

rvalue ссылки

Одним из самых значительных изменений в ядре языка по праву можно считать введение rvalue ссылок. Они получили свое имя по аналогии с давно знакомыми любому С++ программисту ссылками. Теперь эти, "старые" ссылки именуются не иначе как lvalue ссылки. Чем же отличаются rvalue ссылки от своих предшественниц lvalue ссылок? Во-первых, отличие в способе записи, если lvalue ссылки используют лексему "& "(амперсанд) для своей декларации, то rvalue использует двойной амперсанд "&& ". Кто-то может воскликнуть, так это же "ссылка на ссылку!"; да, это выглядит именно так и многие были недовольны подобной нотацией, считая, что это может смутить конечных пользователей. Но нас так просто не смутишь, правда? Во-вторых, они отличаются по типам объектов, на которые они могут ссылаться:

Type& //может ссылаться на любое не константное lvalue. const Type& //может ссылаться на любое выражение. Type&& //может ссылаться на не константные xvalue и prvalue. const Type&& //может ссылаться на любое выражение, кроме lvalue.

Для лучшего усвоения, давайте рассмотрим следующий пример:

Int&& xvalue_func(); int& lvalue_func(); int prvalue_func(); int main() { double d = 0.0; const int i = 0; //---Type& //#1:Ok, простое lvalue int& lvalue = lvalue_func(); //#2:Error, lvalue ссылка не может быть привязана к prvalue int& wrong_lvalue1 = prvalue_func(); //#3:Error, lvalue ссылка не может быть привязана к xvalue int& wrong_lvalue2 = xvalue_func(); /* #4:Error, lvalue ссылка на не константу, не может быть привязана к константному выражению */ int& non_const_lvalue = i; /* #5:Error, lvalue ссылка не может быть привязана к переменной, чей интегральный тип не совпадает с типом ссылки */ int& type_mismatch_lvalue = d; //---Type&& //#6:Error, rvalue ссылка не может быть привязана к lvalue int&& rvalue1 = lvalue_func(); //#7:Ok, rvalue ссылка привязывается к xvalue int&& rvalue2 = xvalue_func(); //#8:Ok, rvalue ссылка привязывается к prvalue int&& rvalue3 = prvalue_func(); //#9:Ok, rvalue ссылка привязывается к prvalue int&& rvalue4 = 0; /* #10:Error, rvalue ссылка на не константу, не может быть привязана к константному выражению */ int&& non_const_rvalue = i; /* #11:Ok, rvalue ссылка может быть привязана к переменной, чей интегральный тип не совпадает с типом ссылки */ int&& type_mismatch_rvalue = d; //---const Type& //#12:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue1 = lvalue_func(); //#13:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue2 = prvalue_func(); //#14:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue3 = xvalue_func(); //#15:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue4 = i; //#16:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue5 = d; //#17:Ok, const Type& может быть привязано к любому выражению const int& const_lvalue6 = 0; //---const Type&& //#18:Error, const Type&& не может быть привязано к lvalue const int&& const_rvalue1 = lvalue_func(); //#19:Ok, const Type&& может быть привязано к prvalue const int&& const_rvalue2 = prvalue_func(); //#20:Ok, const Type&& может быть привязано к xvalue const int&& const_rvalue3 = xvalue_func(); //#21:Error, const Type&& не может быть привязано к lvalue const int&& const_rvalue4 = i; /* #22:Ok, const Type&& может быть привязано к выражению, чей интегральный тип не совпадает с типом ссылки */ const int&& const_rvalue5 = d; } int&& xvalue_func() { return 5; } int& lvalue_func() { static int i = 0; return i; } int prvalue_func() { return 5; }

Обратите внимание на функцию xvalue_func(), она приведена лишь для иллюстрации возврата rvalue-ссылки. Она является небезопасной, т.к. возвращает ссылку на локальный объект. Никогда так не пишите в реальном коде!

Некоторые пункты, я считаю, требуют пояснения. Например #5 : в этом пункте происходит следующее, выражение d является lvalue, а переменная d имеет тип double. В то же время ссылка имеет тип int. Таким образом, чтобы убрать различие в типах необходимо сконвертировать d в int. Но после конвертации получается временный объект типа int, и в результате выражение из lvalue превращается в prvalue! А, как мы уже знаем, lvalue ссылка не может быть привязана к prvalue. Это справедливо не только для интегральных типов, все это справедливо и для классов в той же мере. Для легкости определения таких "узких" мест можно пользоваться простым вопросом: "Что будет результатом выражения?". Еще один пример к этому пункту:

/* "Hello", непосредственно, является lvalue, но в результате исполнения выражения будет создан временный объект std::string и выражение приобретет тип prvalue */ std::string& lvalue_ref = "Hello!";

Вышеизложенное так же объясняет пункты #11 и #22

Правила перегрузки

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

Здесь все достаточно просто:

1. lvalue жестко пытаются привязаться к lvalue ссылкам

2. rvalue жестко пытаются привязаться к rvalue ссылкам

3. Модифицируемые lvalue или rvalue мягко пытаются привязаться к не константным ссылкам на lvalue и rvalue соответственно.

Поясняющий пример:

#include int&& xvalue_func(); int& lvalue_func(); int prvalue_func(); const int const_prvalue_func(); void foo(int& arg); void foo(const int& arg); void foo(int&& arg); void foo(const int&& arg); int main() { const int i = 0; foo(xvalue_func()); foo(lvalue_func()); foo(prvalue_func()); foo(const_prvalue_func()); foo(i); foo(5); std::cin.get(); } void foo(int& arg) { std::cout << "void foo(int& arg)" << std::endl; } void foo(const int& arg) { std::cout << "void foo(const int& arg)" << std::endl; } void foo(int&& arg) { std::cout << "void foo(int&& arg)" << std::endl; } void foo(const int&& arg) { std::cout << "void foo(const int&& arg)" << std::endl; } int&& xvalue_func() { return 5; } int& lvalue_func() { static int i = 0; return i; } int prvalue_func() { return 5; } const int const_prvalue_func() { return 5; }

Void foo(int&& arg) void foo(int& arg) void foo(int&& arg) void foo(const int&& arg) void foo(const int& arg) void foo(int&& arg)

Правило "свертки"

Вместо слов, я сразу приведу пример, чтобы понять к каким выражениям применяема свертка:

Typedef int& IntRef; void foo(IntRef&);

Таким образом мы имеем, что тип передаваемый в функции foo является ссылкой на ссылку или int& &(не путать с rvalue &&!), чего не может быть в текущей версии С++. Именно для таких ситуаций было придумано "правило свертки", применимое к typedef типам, шаблонам и типам, полученным помощью decltype. Введем понятие выведенный тип ,для упрощения дальнейших объяснений. Выведенный тип - Это тип полученный посредством оператора decltype , определения с помощью оператора typedef или являющийся параметром шаблона.

Модификатором выведенного типа будем называть модификатор ссылки(& или &&), который используется в объявлении типа, например:

Typedef int& IntRef; template

Правило можно выразить следующим образом: Если выведенный тип содержит модификатор rvalue ссылки, тогда результирующий тип будет являться rvalue ссылкой, тогда и только тогда, когда применяемый модификатор ссылки есть rvalue модификатор(&&), в противном случае результирующим типом будет являться lvalue ссылка. Если выведенный тип содержит модификатор lvalue ссылки, тогда какой-бы не был применен модификатор ссылки к выведенному типу результирующий тип останется lvalue ссылкой.

Пример применения правила:

Семантика перемещения(move semantic )

Наконец, перейдем к практическому применению изложенного выше материала - семантике перемещения. Это одно из самых важных и нужных новшеств, которые привнесли rvalue ссылки в C++(для программистов не занимающихся написанием библиотек общего назначения самое важное, я полагаю). Итак, в чем же оно заключается? Как мы уже выяснили, параметр являющийся rvalue ссылкой находится "на последнем издыхании" и следовательно его ресурсы уже готовы к тому, чтобы быть освобожденными. Как мы можем это использовать, спросите вы? Очень просто - если есть некий объект, чьи ресурсы более не нужны мы можем забрать(steal) эти ресурсы себе! Не впечатлены? Правильно, необходимо привести пример, чтобы понять что происходит и, главное, зачем это надо:

Class NodePrivate { friend class Node; std::vector m_List1; std::vector m_List2; std::vector m_List3; std::vector m_List4; std::vector m_List5; std::string m_strVeryLongString; public: NodePrivate(const NodePrivate& Rhs); } class Node { std::unique_ptr m_spData; public: Node& operator=(const Node& Rhs); Node& operator=(Node&& Rhs); }; NodePrivate::NodePrivate(const NodePrivate& Rhs) { m_List1 = Rhs.m_List1; m_List2 = Rhs.m_List2; m_List3 = Rhs.m_List3; m_List4 = Rhs.m_List4; m_List5 = Rhs.m_List5; m_strVeryLongString = Rhs.m_strVeryLongString; } Node& Node::operator=(const Node& Rhs) { /* Тут происходит полное копирование всех внутренних данных - векторов, строк и т.д. Очень затратная операция. */ m_spData.reset(new NodePrivate(*Rhs.m_spData)); } Node& Node::operator=(Node&& Rhs) { //Просто перемещаем указатель, никакого копирования! m_spData = std::move(Rhs.m_spData); }

Из примера выше можно заметить, что при использование operator= с семантикой перемещения(а это достигается передачей rvalue ссылки качестве параметра) происходить только перемещение указателя(кстати std::unique_ptr это замена std::auto_ptr из предыдущего стандарта, особенностью этого указателя является отсутствие operator= копирования и присутствие operator= перемещения) не выполняется никакого глубокого копирования. Это операция тривиальна и выполняется довольно быстро, чего нельзя сказать о полном копировании всех данных содержащихся в объекте класса NodePrivate происходящем, при использовании operator=(const Node& Rhs).

Кстати, поддержка оператора перемещения добавлена в классы stl и если в вашем классе есть этот оператор то и все stl члены, будут перемещены в результате вызова оператора перемещения! Более того, так как stl знает о семантике перемещения это может дать прирост в производительности при исполнении некоторых операций stl(все мы знаем, что stl очень любит копировать объекты, но с приходом семантики перемещения копирование может быть заменено на перемещение). Это даст ощутимый прирост в производительности с минимальными усилиями с вашей стороны, C++ не зря считается одним из самых "производительных" языков.

С пришествием семантики перемещения мы получили еще одно средство сделать наши программы быстрее. Ведь теперь объекты, которые прекращают свое существование, могут быть использованы без избыточного копирования. Это за нас сделает умный компилятор, а там где компилятор пасует(например если объект не находится на пороге уничтожения, но вы знаете, что в дальнейшем его использования не предвидится) вы можете использовать std::move для перемещения ресурсов, занимаемых этим объектом. Но с этим надо быть осторожным, т.к. вы должны гарантировать, что никто в дальнейшем не будет использовать объект, который был перемещение в противном случае получите "падение" программы.

Можно привести пример, более приближенный к реальности:

Std::vector ReadDataFromSocket(boost::asio::ip::tcp::socket& Socket) { ... // Надо прочитать крупный массив данных из сокета std::vector Array(BigLength); ... Socket.read_some(boost::asio::buffer(Array)); ... // Перемещаем вектор, избавляясь от тяжеловесного копирования return std::move(Array); }

*std::move, здесь, используется только для наглядности. Его использование не является обязательным в данном случае.

Элегантно, не правда ли?

Если конструктор перемещения явно не объявлен для класса A, тогда он будет сгенерирован неявно при соблюдении следующих условий(С++11 12.8.9):

Класс A не содержит явного объявления конструктора копирования
Класс A не содержит явного объявления оператора копирования
Класс A не содержит явного объявления оператора перемещения
Класс A не содержит явного объявления деструктора
Конструктора копирования не был явно помечен как deleted

Если конструктор перемещения не был объявлен явно или не был сгенерирован компилятором, тогда при использовании семантики перемещения будет использован конструктор копирования.

Совершенная передача(perfect forwarding )

Прежде чем описать что же это, такое вернемся к предыдущему стандарту и опишем существующую проблему. Предположим, у нас есть шаблонная функция foo принимающая один параметр, и передающая его функции bar(T& something) :

Template void foo(T& Object) { bar(Object); }

Итак, все хорошо. Но, что если мы захотим передать, скажем, число 100 в качестве аргумента функции?

Не беда, напишем так:

Template void foo(const T& Object) { bar(Object);//Ooops }

Но в этом случае будет ошибка компиляции, т.к. bar принимает не константную ссылку. Значит надо предоставить 2 функции bar - константную и нет. А теперь представим, что у функции не один параметр а 2,3, или 5? Получается, что подобная задача очень трудна в реализации, т.к мы имеем (2^n - 1) перегруженных функций, где n - количество аргументов функции. Если вы думаете, что такое количество параметров является плохим стилем и вообще так никто не пишет, тогда обратите свой взор на std::bind, std::make_shared и т.д.

Теперь посмотрим, какое же решение нам предоставляет новый стандарт:

Template void foo(T&& Object) { bar(std::forward(Object)); }

Используя вышеприведённый код проблема с передачей параметров полностью решается, это и называется совершенной передачей , т.к. тип аргумента сохраняется между вызовами внешней функции foo и внутренней функции bar . Больше нет нужды в перегрузке кучи функций - разработчики обобщенного кода могут быть довольны.

Это решение возможно благодаря тому, что если параметром шаблона является T&&, то переданный тип сохранит себя, а std::forward нужен затем, что любой именованный тип внутри функции foo превращается в lvalue, а нам нужен исходный тип - для этого и применяется std::forward он сохраняет исходный тип аргумента и лишает его имени(получается T&&), что позволяет в дальнейшем передать его в точности в функцию bar .

Почему же T&& сохраняет исходный тип? Это происходит согласно правилам вывода параметров шаблона, которые

а) Исключает ссылки из рассмотрения, и выводят тип согласно переданному аргументу и б) специальному правилу(14.8.2.1/3) касающемуся T&& и lvalue - эта пара на выходе дает T&(на const T&& это правило не распространяется!). Рассмотрим это на примере функции

Template void foo(T&&);

Передаваемый аргумент

Выведенный параметр шаблона

Результирующий тип аргумента функции

int&(см. 14.8.2.1/3)

int & && -> int&

const lvalue int

const int & && - > const int&

const rvalue int

Ну или все тоже самое, только в коде:

Template struct Foo << "foo(): plain" << std::endl; } }; template<> struct Foo { static void foo() { std::cout << "foo(): int&" << std::endl; } }; template<> struct Foo { static void foo() { std::cout << "foo(): const int&" << std::endl; } }; template<> struct Foo { static void foo() { std::cout << "foo(): int" << std::endl; } }; template<> struct Foo { static void foo() { std::cout << "foo(): const int" << std::endl; } }; int bar() { return 1; } const int const_bar() { return 1; } template void helper(T&&) { Foo::foo(); } int main() { int i = 1; const int j = 1; helper(i); helper(j); helper(bar()); helper(const_bar()); }

Будет выведено:

Foo(): int& foo(): const int& foo(): int foo(): const int

Что и требовалось доказать.

Вывод

С появлением rvalue ссылок у нас появилась возможность сделать наш код семантически более правильным, а также привнести в него дополнительную скорость за счёт семантики перемещения. Разработчики библиотек получили надежное средство для передачи параметров во внутренние функции без чудовищного количества перегруженных функций. Поэтому rvalue ссылку могут быть по праву признаны одним из самых важных нововведений в ядре языка. Хотя добавление перемещающих operator= и конструктора связаны с нескорыми сложностями(написание нового кода, использование этого кода) я советую всем привыкать писать их, т.к. это сделает ваш код быстрее. Даже если вы явно нигде это не используете, это уже используется в stl и, я уверен, будет использоваться во всех известных библиотеках, которыми занимаются сознательные разработчики, тем более, что rvalue ссылки уже сейчас поддерживаются главными С++ компиляторами (gcc и MSVC). А раз ничего не останавливает нас от использования подобных техник - давайте их использовать!

Мне лично в языке 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 будет весьма нелишним при использовании

За счет которых возможно реализовать отдельные функции для обработки обычных (живущих дольше выражения) и временных объектов. Такой подход позволяет написать более оптимальный код, т.к. зная, что аргументом является временный объект (который все равно будет уничтожен после обработки), мы можем использовать данные этого объекта, вместо создания новых. Такое поведение называется семантикой перемещения, для его реализации служат перемещающие конструкторы, принимающие на вход 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; }

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



  • Есть вопросы?

    Сообщить об опечатке

    Текст, который будет отправлен нашим редакторам: