Конструкторы move и операторы присваивания move (C++)
В этом разделе описывается запись конструктора перемещения и оператора назначения перемещения для класса C++. Конструктор перемещения позволяет переместить ресурсы, принадлежащие объекту rvalue, в lvalue без копирования. Дополнительные сведения о семантике перемещения см. в статье Rvalue Reference Declarator: &&
Этот раздел построен на основе приведенного ниже класса C++ MemoryBlock , который управляет буфером памяти.
// MemoryBlock.h #pragma once #include #include class MemoryBlock < public: // Simple constructor that initializes the resource. explicit MemoryBlock(size_t length) : _length(length) , _data(new int[length]) < std::cout // Destructor. ~MemoryBlock() < std::cout std::cout // Copy constructor. MemoryBlock(const MemoryBlock& other) : _length(other._length) , _data(new int[other._length]) < std::cout // Copy assignment operator. MemoryBlock& operator=(const MemoryBlock& other) < std::cout return *this; > // Retrieves the length of the data resource. size_t Length() const < return _length; >private: size_t _length; // The length of the resource. int* _data; // The resource. >;
В следующих процедурах описывается создание конструктора перемещения и оператора присваивания перемещения для этого примера класса C++.
Создание конструктора перемещения для класса C++
- Определите пустой метод конструктора, принимающий в качестве параметра ссылку rvalue на тип класса, как показано в следующем примере:
MemoryBlock(MemoryBlock&& other) : _data(nullptr) , _length(0)
_data = other._data; _length = other._length;
other._data = nullptr; other._length = 0;
Создание оператора присваивания перемещения для класса C++
- Определите пустой оператор присваивания, принимающий в качестве параметра ссылку rvalue на тип класса и возвращающий ссылку на тип класса, как показано в следующем примере:
MemoryBlock& operator=(MemoryBlock&& other)
if (this != &other)
// Free the existing resource. delete[] _data;
Выполните шаги 2 и 3 из первой процедуры, чтобы переместить данные-члены из исходного объекта в создаваемый объект:
// Copy the data pointer and its length from the // source object. _data = other._data; _length = other._length; // Release the data pointer from the source object so that // the destructor does not free the memory multiple times. other._data = nullptr; other._length = 0;
return *this;
Пример. Полный конструктор перемещения и оператор назначения
В следующем примере показаны полные конструктор перемещения и оператор назначения перемещения для класса MemoryBlock :
// Move constructor. MemoryBlock(MemoryBlock&& other) noexcept : _data(nullptr) , _length(0) < std::cout // Move assignment operator. MemoryBlock& operator=(MemoryBlock&& other) noexcept < std::cout return *this; >
Пример использования семантики перемещения для повышения производительности
В следующем примере показано, как семантика перемещения может повысить производительность приложений. В примере добавляются два элемента в объект-вектор, а затем вставляется новый элемент между двумя существующими элементами. Класс vector использует семантику перемещения для эффективного выполнения операции вставки, перемещая элементы вектора вместо копирования.
// rvalue-references-move-semantics.cpp // compile with: /EHsc #include "MemoryBlock.h" #include using namespace std; int main() < // Create a vector object and add a few elements to it. vectorv; v.push_back(MemoryBlock(25)); v.push_back(MemoryBlock(75)); // Insert a new element into the second position of the vector. v.insert(v.begin() + 1, MemoryBlock(50)); >
В примере получается следующий вывод.
In MemoryBlock(size_t). length = 25. In MemoryBlock(MemoryBlock&&). length = 25. Moving resource. In ~MemoryBlock(). length = 0. In MemoryBlock(size_t). length = 75. In MemoryBlock(MemoryBlock&&). length = 75. Moving resource. In MemoryBlock(MemoryBlock&&). length = 25. Moving resource. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 0. In MemoryBlock(size_t). length = 50. In MemoryBlock(MemoryBlock&&). length = 50. Moving resource. In MemoryBlock(MemoryBlock&&). length = 25. Moving resource. In MemoryBlock(MemoryBlock&&). length = 75. Moving resource. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 0. In ~MemoryBlock(). length = 25. Deleting resource. In ~MemoryBlock(). length = 50. Deleting resource. In ~MemoryBlock(). length = 75. Deleting resource.
Перед Visual Studio 2010 в этом примере выводятся следующие выходные данные:
In MemoryBlock(size_t). length = 25. In MemoryBlock(const MemoryBlock&). length = 25. Copying resource. In ~MemoryBlock(). length = 25. Deleting resource. In MemoryBlock(size_t). length = 75. In MemoryBlock(const MemoryBlock&). length = 25. Copying resource. In ~MemoryBlock(). length = 25. Deleting resource. In MemoryBlock(const MemoryBlock&). length = 75. Copying resource. In ~MemoryBlock(). length = 75. Deleting resource. In MemoryBlock(size_t). length = 50. In MemoryBlock(const MemoryBlock&). length = 50. Copying resource. In MemoryBlock(const MemoryBlock&). length = 50. Copying resource. In operator=(const MemoryBlock&). length = 75. Copying resource. In operator=(const MemoryBlock&). length = 50. Copying resource. In ~MemoryBlock(). length = 50. Deleting resource. In ~MemoryBlock(). length = 50. Deleting resource. In ~MemoryBlock(). length = 25. Deleting resource. In ~MemoryBlock(). length = 50. Deleting resource. In ~MemoryBlock(). length = 75. Deleting resource.
Версия этого примера, в которой используется семантика перемещения, более эффективна, чем версия, в которой эта семантика не используется, поскольку в ней выполняется меньше операций копирования, выделения памяти и освобождения памяти.
Отказоустойчивость
Во избежание утечки ресурсов (таких как память, дескрипторы файлов и сокеты) обязательно освобождайте их в операторе присваивания перемещения.
Чтобы предотвратить невосстановимое уничтожение ресурсов, в операторе присваивания перемещения необходимо правильно обрабатывать присваивания самому себе.
Если для класса определены как конструктор перемещения, так и оператор присваивания перемещения, можно исключить избыточный код, написав конструктор перемещения так, чтобы он вызывал оператор присваивания перемещения. В следующем примере показана измененная версия конструктора перемещения, вызывающая оператор присваивания перемещения:
// Move constructor. MemoryBlock(MemoryBlock&& other) noexcept : _data(nullptr) , _length(0)
Функция std::move преобразует lvalue other в rvalue.
M.3 – Конструкторы перемещения и присваивание перемещением
В уроке «M.1 – Введение в умные указатели и семантику перемещения» мы рассмотрели std::auto_ptr , обсудили необходимость семантики перемещения и рассмотрели некоторые недостатки, которые возникают, когда функции, разработанные для семантики копирования (конструкторы копирования и операторы присваивания копированием) переопределяются для реализации семантики перемещения.
В этом уроке мы более подробно рассмотрим, как C++11 решает эти проблемы с помощью конструкторов перемещения и присваивания перемещением.
Конструкторы копирования и присваивание копированием
Во-первых, давайте сделаем обзор семантики копирования.
Конструкторы копирования используются для инициализации класса путем создания копии объекта того же класса. Присваивание копированием используется для копирования одного объекта класса в другой существующий объект класса. По умолчанию, если конструктор копирования и оператор присваивания копированием не указаны явно, C++ предоставляет их. Эти предоставляемые компилятором функции создают поверхностные копии, что может вызывать проблемы для классов, динамически выделяющих память. Таким образом, классы, которые имеют дело с динамической памятью, должны переопределять эти функции для создания глубоких копий.
Возвращаясь к нашему примеру класса умного указателя Auto_ptr из первого урока этой главы, давайте рассмотрим версию, которая реализует конструктор копирования и оператор присваивания копированием, которые делают глубокие копии, и пример программы, которая их проверяет:
template class Auto_ptr3 < T* m_ptr; public: Auto_ptr3(T* ptr = nullptr) :m_ptr(ptr) < >~Auto_ptr3() < delete m_ptr; >// Конструктор копирования // Выполняем глубокое копирование a.m_ptr в m_ptr Auto_ptr3(const Auto_ptr3& a) < m_ptr = new T; *m_ptr = *a.m_ptr; >// Присваивание копированием // Выполняем глубокое копирование a.m_ptr в m_ptr Auto_ptr3& operator=(const Auto_ptr3& a) < // Обнаружение самоприсваивания if (&a == this) return *this; // Освобождаем любые ресурсы, которые уже храним delete m_ptr; // Копируем ресурс m_ptr = new T; *m_ptr = *a.m_ptr; return *this; >T& operator*() const < return *m_ptr; >T* operator->() const < return m_ptr; >bool isNull() const < return m_ptr == nullptr; >>; class Resource < public: Resource() < std::cout ~Resource() < std::cout >; Auto_ptr3 generateResource() < Auto_ptr3res(new Resource); return res; // это возвращаемое значение вызовет конструктор копирования > int main() < Auto_ptr3mainres; mainres = generateResource(); // это присваивание вызовет присваивание копированием return 0; >
В этой программе мы используем функцию с именем generateResource() для создания умного указателя, инкапсулирующего ресурс, который затем передается обратно в функцию main() . Затем функция main() присваивает его существующему объекту Auto_ptr3 .
Когда эта программа запускается, она печатает:
Resource acquired Resource acquired Resource destroyed Resource acquired Resource destroyed Resource destroyed
(Примечание: вы можете получить только 4 сообщения, если ваш компилятор исключает копирование возвращаемого значения из функции generateResource() )
Для такой простой программы происходит слишком много созданий и уничтожений объектов Resource ! Что тут происходит?
Давайте рассмотрим подробнее. В этой программе выполняется 6 ключевых шагов (по одному для каждого напечатанного сообщения):
- Внутри generateResource() создается локальная переменная res , которая инициализируется динамически размещаемым объектом Resource , что приводит к первому сообщению » Resource acquired «.
- res возвращается обратно в main() по значению. Здесь мы возвращаем по значению, потому что res – это локальная переменная, она не может быть возвращена по адресу или по ссылке, потому что будет уничтожена при завершении generateResource() . Таким образом, res копируется конструктором во временный объект. Поскольку наш конструктор копирования выполняет глубокое копирование, здесь выделяется новый Resource , что приводит ко второму сообщению » Resource acquired «.
- res выходит за пределы области видимости, уничтожая первоначально созданный Resource , что приводит к первому сообщению » Resource destroyed «.
- Этот временный объект присваивается mainres путем присваивания копированием. Поскольку наше присваивание копированием также выполняет глубокое копирование, размещается новый Resource , вызывая еще одно сообщение » Resource acquired «.
- Выражение присваивания завершается, и временный объект выходит за пределы области действия выражения и уничтожается, вызывая сообщение » Resource destroyed «.
- В конце main() переменная mainres выходит из области видимости, и отображается наше последнее сообщение » Resource destroyed «.
Короче говоря, поскольку мы вызываем конструктор копирования один раз, чтобы скопировать res во временный объект, и один раз присваивание копированием для копирования временного объекта в mainres , в итоге мы размещаем и уничтожаем в общей сложности 3 отдельных объекта.
Неэффективно, но, по крайней мере, не дает сбоев!
Однако с семантикой перемещения мы можем добиться большего.
Конструкторы перемещения и присваивание перемещением
C++11 определяет две новые функции, обслуживающие семантику перемещения: конструктор перемещения и оператор присваивания перемещением. В то время как цель конструктора копирования и присваивания копированием – выполнить копирование одного объекта в другой, цель конструктора перемещения и присваивания перемещением – передать владение ресурсами от одного объекта к другому (что обычно намного дешевле, чем создание копии).
Определение конструктора перемещения и присваивания перемещением работают аналогично их аналогам для копирования. Однако в то время как копирующие версии этих функций принимают в качестве параметра константную lvalue-ссылку, перемещающие версии этих функций используют в качестве параметра неконстантные rvalue-ссылки.
Вот тот же класс Auto_ptr3 , что и выше, с добавленными конструктором перемещения и оператором присваивания перемещением. Для сравнения мы оставили выполняющие глубокое копирование конструктор копирования и оператор присваивания копированием.
#include template class Auto_ptr4 < T* m_ptr; public: Auto_ptr4(T* ptr = nullptr) :m_ptr(ptr) < >~Auto_ptr4() < delete m_ptr; >// Конструктор копирования // Выполняем глубокое копирование a.m_ptr в m_ptr Auto_ptr4(const Auto_ptr4& a) < m_ptr = new T; *m_ptr = *a.m_ptr; >// Конструктор перемещения // Передача владения a.m_ptr в m_ptr Auto_ptr4(Auto_ptr4&& a) noexcept : m_ptr(a.m_ptr) < a.m_ptr = nullptr; // поговорим об этой строке подробнее ниже >// Присваивание копированием // Выполняем глубокое копирование a.m_ptr в m_pt Auto_ptr4& operator=(const Auto_ptr4& a) < // Обнаружение самоприсваивания if (&a == this) return *this; // Освобождаем любые ресурсы, которые уже храним delete m_ptr; // Копируем ресурс m_ptr = new T; *m_ptr = *a.m_ptr; return *this; >// Присваивание перемещением // Передача владения a.m_ptr в m_ptr Auto_ptr4& operator=(Auto_ptr4&& a) noexcept < // Обнаружение самоприсваивания if (&a == this) return *this; // Освобождаем любые ресурсы, которые уже храним delete m_ptr; // Передаем владение a.m_ptr в m_ptr m_ptr = a.m_ptr; a.m_ptr = nullptr; // поговорим об этой строке подробнее ниже return *this; >T& operator*() const < return *m_ptr; >T* operator->() const < return m_ptr; >bool isNull() const < return m_ptr == nullptr; >>; class Resource < public: Resource() < std::cout ~Resource() < std::cout >; Auto_ptr4 generateResource() < Auto_ptr4res(new Resource); return res; // это возвращаемое значение вызовет конструктор перемещения > int main() < Auto_ptr4mainres; mainres = generateResource(); // это присваивание вызовет присваивание перемещением return 0; >
Конструктор перемещения и оператор присваивания перемещением просты. Вместо того, чтобы выполнять глубокое копирование исходного объект ( а ) в неявный объект this , мы просто перемещаем (крадем) ресурсы исходного объекта. Это включает в себя поверхностное копирование указателя исходного объекта в неявный объект this с последующей установкой для указателя исходного объекта значения nullptr .
При запуске эта программа печатает:
Resource acquired Resource destroyed
Это намного лучше!
Ход программы точно такой же, как и раньше. Однако вместо вызова конструктора копирования и оператора присваивания копированием эта программа вызывает конструктор перемещения и оператор присваивания перемещением. Рассмотрим немного подробнее:
- Внутри generateResource() создается локальная переменная res , которая инициализируется динамически размещаемым объектом Resource , что приводит к первому сообщению » Resource acquired «.
- res возвращается обратно в main() по значению. res перемещается конструктором во временный объект, передавая динамически созданный объект, хранящийся в res , во временный объект. О том, почему это происходит, мы поговорим ниже.
- res выходит из области видимости. Поскольку res больше не управляет указателем (он был перемещен во временный объект), здесь ничего интересного не происходит.
- Временный объект перемещается присваиванием в mainres . Это переносит динамически созданный объект, хранящийся во временном объекте, в mainres .
- Выражение присваивания завершается, временный объект выходит за пределы области действия выражения и уничтожается. Однако, поскольку временный объект больше не управляет указателем (он был перемещен в mainres ), здесь также не происходит ничего интересного.
- В конце main() переменная mainres выходит из области видимости, и отображается наше последнее сообщение » Resource destroyed «.
Поэтому вместо того, чтобы копировать наш объект Resource дважды (один раз для конструктора копирования и один раз для присваивания копированием), мы дважды перемещаем его. Это более эффективно, поскольку объект Resource создается и уничтожается только один раз, а не три раза.
Когда вызываются конструктор перемещения и присваивание перемещением?
Конструктор перемещения и присваивание перемещением вызываются, когда эти функции определены, а аргументом для построения или присваивания является r-значение. Чаще всего это r-значение будет литералом или временным значением.
В большинстве случаев конструктор перемещения и оператор присваивания перемещением не предоставляются по умолчанию, если в классе нет определенных конструкторов копирования, присваивания копированием, присваивания перемещением или деструкторов. Однако дефолтные конструктор перемещения и присваивание перемещением делают то же самое, что и дефолтные конструктор копирования и присваивание копированием (делать копии, а не перемещают).
Правило
Если вам нужен конструктор перемещения и присваивание перемещением, выполняющее перемещения, вам нужно будет написать их самостоятельно.
Ключевой момент в семантике перемещения
Теперь у вас достаточно контекста для понимания ключевой идеи семантики перемещения.
Если мы создаем объект или выполняем присваивание, в котором аргументом является l-значение, единственное разумное, что мы можем сделать, – это скопировать l-значение. Мы не можем предположить, что изменение l-значения безопасно, потому что позже в программе оно может быть снова использовано. Если у нас есть выражение a = b , мы не можем ожидать каких-либо изменений b .
Однако, если мы создаем объект или выполняем присваивание, в котором аргументом является r-значение, тогда мы знаем, что r-значение – это всего лишь временный объект какого-то типа. Вместо того, чтобы копировать его (что может быть дорогостоящим), мы можем просто передать его ресурсы (что дешево) объекту, который мы создаем или которому выполняем присваивание. Это безопасно, потому что временный объект в любом случае будет уничтожен в конце выражения, поэтому мы знаем, что он больше никогда не будет использоваться!
C++11, через rvalue-ссылки, дает нам возможность обеспечивать различное поведение, когда аргументом является r-значение или l-значение, что позволяет нам принимать более разумные и эффективные решения о том, как должны вести себя наши объекты.
Функции перемещения должны всегда оставлять оба объекта в четко определенном состоянии.
В приведенных выше примерах и конструктор перемещения, и функции присваивания перемещением устанавливают a.m_ptr в значение nullptr . Это может показаться лишним – в конце концов, если a – временное r-значение, зачем беспокоиться о выполнении «очистки», если параметр a всё равно будет уничтожен?
Ответ прост: когда a выходит за пределы области видимости, вызывается деструктор a , и a.m_ptr удаляется. Если в этот момент a.m_ptr всё еще указывает на тот же объект, что и m_ptr , тогда m_ptr останется висячим указателем. Когда объект, содержащий m_ptr , в конечном итоге будет использован (или уничтожен), мы получим неопределенное поведение.
Кроме того, в следующем уроке мы увидим случаи, когда a может быть l-значением. В таком случае a не будет уничтожен немедленно, и его можно будет запросить еще до того, как истечет время его жизни.
Автоматические l-значения, возвращаемые по значению, могут быть перемещены вместо копирования
В функции generateResource() в примере выше с Auto_ptr4 , когда переменная res возвращается по значению, она перемещается, а не копируется, даже если res является l-значением. В спецификации C++ есть специальное правило, согласно которому автоматические объекты, возвращаемые функцией по значению, можно перемещать, даже если они являются l-значениями. Это имеет смысл, так как res всё равно будет уничтожен в конце функции! С таким же успехом мы могли бы забрать его ресурсы, вместо того, чтобы выполнять дорогостоящее и ненужное копирование.
Хотя компилятор может перемещать возвращаемые l-значения, в некоторых случаях он может добиться еще большего, просто полностью исключив копирование (что позволяет вовсе избежать необходимости выполнять копирование или перемещение). В таком случае не будут вызываться ни конструктор копирования, ни конструктор перемещения.
Отключение копирования
В приведенном выше классе Auto_ptr 4 мы оставили для сравнения конструктор копирования и оператор присваивания. Но в классах с поддержкой перемещения иногда желательно удалить функции конструктора копирования и присваивания копированием, чтобы гарантировать, что копии не будут созданы. В случае с нашим классом Auto_ptr мы не хотим копировать наш шаблонный объект T – потому что это дорого, и класс T может даже не поддерживать копирование!
Вот версия Auto_ptr , которая поддерживает семантику перемещения, но не поддерживает семантику копирования:
#include template class Auto_ptr5 < T* m_ptr; public: Auto_ptr5(T* ptr = nullptr) :m_ptr(ptr) < >~Auto_ptr5() < delete m_ptr; >// Конструктор копирования - копирование запрещено! Auto_ptr5(const Auto_ptr5& a) = delete; // Конструктор перемещения // Передача владения a.m_ptr в m_ptr Auto_ptr5(Auto_ptr5&& a) noexcept : m_ptr(a.m_ptr) < a.m_ptr = nullptr; >// Присваивание копированием - копирование запрещено! Auto_ptr5& operator=(const Auto_ptr5& a) = delete; // Присваивание перемещением // Передача владения a.m_ptr в m_ptr Auto_ptr5& operator=(Auto_ptr5&& a) noexcept < // Обнаружение самоприсваивания if (&a == this) return *this; // Освобождаем любые ресурсы, которые уже храним delete m_ptr; // Передаем владение a.m_ptr в m_ptr m_ptr = a.m_ptr; a.m_ptr = nullptr; return *this; >T& operator*() const < return *m_ptr; >T* operator->() const < return m_ptr; >bool isNull() const < return m_ptr == nullptr; >>;
Если бы вы попытались передать функции l-значение Auto_ptr5 по значению, компилятор пожаловался бы, что конструктор копирования, необходимый для инициализации аргумента функции, был удален. Это хорошо, потому что мы, вероятно, всё равно должны передавать Auto_ptr5 по константной lvalue-ссылке!
Auto_ptr5 – это (наконец) хороший класс умных указателей. И на самом деле стандартная библиотека содержит класс, очень похожий на этот (и который вы должны использовать вместо этого), с именем std::unique_ptr . Подробнее об std::unique_ptr мы поговорим в этой главе позже.
Еще один пример
Давайте посмотрим на другой класс, который использует динамическую память: простой динамический шаблонный массив. Этот класс содержит конструктор копирования и оператор присваивания копированием, выполняющие глубокое копирование.
#include template class DynamicArray < private: T* m_array; int m_length; public: DynamicArray(int length) : m_array(new T[length]), m_length(length) < >~DynamicArray() < delete[] m_array; >// Конструктор копирования DynamicArray(const DynamicArray &arr) : m_length(arr.m_length) < m_array = new T[m_length]; for (int i = 0; i < m_length; ++i) m_array[i] = arr.m_array[i]; >// Присваивание копированием DynamicArray& operator=(const DynamicArray &arr) < if (&arr == this) return *this; delete[] m_array; m_length = arr.m_length; m_array = new T[m_length]; for (int i = 0; i < m_length; ++i) m_array[i] = arr.m_array[i]; return *this; >int getLength() const < return m_length; >T& operator[](int index) < return m_array[index]; >const T& operator[](int index) const < return m_array[index]; >>;
Теперь давайте, используем этот класс в программе, чтобы показать, как работает этот класс, когда мы размещаем миллион целых чисел в куче. Мы собираемся использовать класс Timer , который мы разработали в уроке «12.18 – Определение времени выполнения кода». Мы будем использовать его, чтобы измерить скорость выполнения нашего кода и показать вам разницу в производительности между копированием и перемещением.
#include #include // для функций std::chrono // Использует показанный выше класс DynamicArray class Timer < private: // Псевдонимы типа, чтобы упростить доступ к вложенному типу using clock_t = std::chrono::high_resolution_clock; using second_t = std::chrono::duration>; std::chrono::time_point m_beg; public: Timer() : m_beg(clock_t::now()) < >void reset() < m_beg = clock_t::now(); >double elapsed() const < return std::chrono::duration_cast(clock_t::now() - m_beg).count(); > >; // Возвращаем копию arr со всеми удвоенными значениями DynamicArray cloneArrayAndDouble(const DynamicArray &arr) < DynamicArraydbl(arr.getLength()); for (int i = 0; i < arr.getLength(); ++i) dbl[i] = arr[i] * 2; return dbl; >int main() < Timer t; DynamicArrayarr(1000000); for (int i = 0; i
На одной из машин автора в режиме релиза эта программа выполнилась за 0,00825559 секунды.
Теперь давайте снова запустим эту же программу, заменив конструктор копирования и присваивание копированием конструктором перемещения и присваиванием перемещением.
#include #include // для функций std::chrono template class DynamicArray < private: T* m_array; int m_length; public: DynamicArray(int length) : m_array(new T[length]), m_length(length) < >~DynamicArray() < delete[] m_array; >// Конструктор копирования DynamicArray(const DynamicArray &arr) = delete; // Присваивание копированием DynamicArray& operator=(const DynamicArray &arr) = delete; // Конструктор перемещения DynamicArray(DynamicArray &&arr) noexcept : m_length(arr.m_length), m_array(arr.m_array) < arr.m_length = 0; arr.m_array = nullptr; >// Присваивание перемещением DynamicArray& operator=(DynamicArray &&arr) noexcept < if (&arr == this) return *this; delete[] m_array; m_length = arr.m_length; m_array = arr.m_array; arr.m_length = 0; arr.m_array = nullptr; return *this; >int getLength() const < return m_length; >T& operator[](int index) < return m_array[index]; >const T& operator[](int index) const < return m_array[index]; >>; class Timer < private: // Псевдонимы типа, чтобы упростить доступ к вложенному типу using clock_t = std::chrono::high_resolution_clock; using second_t = std::chrono::duration>; std::chrono::time_point m_beg; public: Timer() : m_beg(clock_t::now()) < >void reset() < m_beg = clock_t::now(); >double elapsed() const < return std::chrono::duration_cast(clock_t::now() - m_beg).count(); > >; // Возвращаем копию arr со всеми удвоенными значениями DynamicArray cloneArrayAndDouble(const DynamicArray &arr) < DynamicArraydbl(arr.getLength()); for (int i = 0; i < arr.getLength(); ++i) dbl[i] = arr[i] * 2; return dbl; >int main() < Timer t; DynamicArrayarr(1000000); for (int i = 0; i
На той же машине эта программа была выполнена за 0,0056 секунды.
Сравним время выполнения этих двух программ, 0,0056 / 0,00825559 = 67,8%. Версия с перемещением была почти на 33% быстрее!
Просто о сложном — move в языке C++
Здравствуйте уважаемые читатели. Данная публикация адресована начинающим разработчикам С++ которые только становятся на путь высокой производительности и "отстрелянных конечностей". Опытные разработчики найдут здесь скорее повторение тех вещей, которые сами мучительно осваивали в те далекие времена, когда в языке С++ появилась возможность удобного перемещения объектов.
Многие из вас уже слышали и надеюсь использовали функцию move() в своих проектах. Если нет, то пришло время с ней познакомиться.
Вопрос: Опять этот move, сколько уже можно? Есть же множество опубликованного материала по этой теме?
Ответ: Да, есть много статей. В свое время учился по ним, в том числе и тут, на Хабре [1, 2]. Но мне все равно было не понятно, значит, учитывая статистику, непонятно также и некоторому количеству читателей.
Как обычно начинаются туториалы по move? Рассмотрим lvalue объект, ему соответствует rvalue объект, между ними есть оператор присваивания (=). Тут появляются ссылки, да не просто, а ссылки на lvalue, на rvalue и пошло-поехало. Мозг перегружается, статья пролистывается до конца. Поэтому попробую рассказать о move c другой стороны - в стиле "от практики к теории" - так, как хотел бы чтобы мне рассказали.
Что обычно говорят о move? Это крутая штука, код с ней работает быстрее. А насколько быстрее? Давайте проверим.
Чтобы оценить быстродействие возьмем следующий класс:
class LogDuration < public: LogDuration(std::string id) : id_(std::move(id)) < >~LogDuration() < const auto end_time = std::chrono::steady_clock::now(); const auto dur = end_time - start_time_; std::cout (dur).count() private: const std::string id_; const std::chrono::steady_clock::time_point start_time_ = std::chrono::steady_clock::now(); >;
Не пугайтесь, он нам будет нужен только как условный секундомер для экспериментов. Чтобы с его помощью оценить время выполнения операции достаточно сделать так:
где фигурные скобки задают область видимости. При выходе за ее пределы запускаются деструкторы классов для объектов (исключение - статичные переменные), которые были созданы внутри данной области, в том числе и ~LogDuration(), который покажет время выполнения операций внутри блока.
Итак, начнем экспериментировать.
Говорят, что для векторов и строк (std::string) нужно по возможности использовать move. Проверим. Напишем такой код:
int main() < vectorbig_vector(1e9, 0); < LogDuration ld("vector copy"); vectorreciever(big_vector); > cout
Здесь мы создаем вектор big_vector из нулей длиной 10^9, а затем создаем новый вектор как копию данного. Время на создание копии выводится в консоль:
vector copy: operation time: 484 ms size of big_vector is 1000000000
Программа valgrind показывает, что за время выполнения программы было использовано 2 ГБ оперативной памяти:
total heap usage: 4 allocs, 4 frees, 2,000,073,728 bytes allocated
Итак, у нас получилось два одинаковых вектора, затрачено полсекунды и 2 ГБ оперативной памяти. Дальше вопрос - а что если исходный вектор нам дальше в коде никогда не понадобится, мы бы сэкономили 1 ГБ. Давайте посмотрим, что будет если добавить move. Произведем замену:
- vector reciever(big_vector); + vector reciever(move(big_vector));
И о чудо! Время выполнения уменьшилось почти в 10 раз, а размер исходного вектора стал равен нулю:
vector move: operation time: 34 ms size of big_vector is 0
Valgrind уже более оптимистичен:
total heap usage: 3 allocs, 3 frees, 1,000,073,728 bytes allocated
Получается, что воспользовавшись move мы выиграли в скорости, но пожертвовали исходным вектором. Случай с длинной строкой вместо вектора предлагаю проверить самостоятельно.
Теперь попробуем разобраться что тут вообще происходит. Давайте напишем свой вектор, точнее простую обертку над стандартным вектором
template class Vector < public: Vector(size_t size, T value) : data_(size, value) < >Vector(const Vector& rhs) < cout Vector(Vector&& rhs) noexcept < cout size_t size() < return data_.size(); >private: vector data_; >;
Также не пугайтесь, тут нужно смотреть на то, что внутри секции public. Добавьте этот код перед main() в вашей программе, а внутри main замените первую букву в слове vector на заглавную везде, где он упоминается. Для случая:
Vector reciever(big_vector);
в консоли будет выведено:
copy constructor was called vector copy: operation time: 0 ms size of big_vector is 1000000000
А для варианта с move:
move constructor was called vector move: operation time: 0 ms size of big_vector is 1000000000
Здесь мы подходим к наблюдению, что функция move сама по себе не выполняет никаких перемещений, несмотря на название, а делает все возможное чтобы в данном конкретном примере вызвать конструктор перемещения - Vector(Vector&& rhs). Т.к. в приведенном классе-обертке в конструкторах выполяется только вывод текста, то понятно, что время операции столь мало, а исходный вектор никуда не исчезает.
Использование move не ограничивается конструкторами классов. Например:
void CopyFoo(string text) <> void CopyRefFoo(const string& text) <> void MoveFoo(string&& text) <> int main() < string text; text = "some text"; CopyRefFoo(text); CopyFoo(text); // MoveFoo(text); // compile error MoveFoo("another text"); MoveFoo(move(text));
Обратите внимание на строчку 12, где закомментирована операция. Сигнатура данной функции содержит "волшебные" символы && из-за которых не получается ей указать объект text. А какую-то бесхозную строку в кавычках можно. Теперь обратите внимание на строку 7, где объекту text присваивается "some text". Чем они различаются принципиально, кроме расположения лево-право от оператора присваивания?
А тем, что text имеет адрес в памяти, а выражение "some text" его не имеет, точнее его адрес не так просто найти. Адрес постоянного объекта можно узнать так:
cout
Теперь смотрите, для того, чтобы функция MoveFoo приняла аргумент, он "не должен иметь адреса", как "another text" например. Такие объекты еще называют временными. Теперь мы можем подойти к тому моменту когда можно сказать, что делает функция move - она делает так, что ее аргумент притворяется "безадресным", т.е. временным, поэтому 14-я строка нормально компилируется. И если внутри функции MoveFoo ничего с text не делать, то он сам по себе никуда не пропадет, не перенесется, не исчезнет. Но зачем же тогда спрашивается все телодвижения? А вот если написать:
void MoveFoo(string&& text)
то после выполнения данной функции переменная text во внешнем блоке окажется пустой (компилятор gcc 7.5 c++17), как в самом начале для случая с перемещением вектора.
Теперь вернемся к вопросу почему исходный вектор "переместился" в новый вектор за такое короткое время?
У нас есть некоторые наблюдения: при использовании move памяти затрачивалось практически вровень размеру исходного массива.
Представим вектор как структуру данных, которая в самом упрощенном варианте хранит адрес (указатель) на место в памяти, где находятся все его элементы. Мы же помним, что в векторе все элементы расположены в памяти последовательно, без разрывов. А вторым полем будет переменная, хранящая текущий размер вектора. Также мы знаем, что после операции "перемещения" исходный вектор оказывается пустым. А теперь представьте, что встречаются два вектора - один с набором из 10^9 элементов, второй пустой. Самое простое решение им взять и "обменяться" своим содержимым. Новый просто изменит свой адрес, указывающий на начало блока данных на тот, что был у исходного. Также обновит свой размер. А исходный примет такие же поля от пустого вектора. Все просто. Если пройтись отладчиком по цепочке от конструктора перемещения, то можно обнаружить такой код в стандартной библиотеке в файле stl_vector.h:
void _M_swap_data(_Vector_impl& __x) _GLIBCXX_NOEXCEPT
Там конечно, все намного сложнее, но общий принцип примерно таков.
Очень надеюсь, что теперь основные моменты использования move для вас прояснились. Дальше рекомендую уже ознакомиться с более научными работами по использованию move семантики, где легко, надеюсь, уловите аналогии с lvalue, rvalue и т.п. А более опытным разработчикам - если дочитали до конца, буду рад услышать Ваши комментарии и замечания.
c++: почему не вызывается конструктор перемещения?
Так этот код не вызывает конструктор перемещения или оператор перемещения, его вывод:
begin constructor by value 10 end destructor 10
При этом, если я напишу:
struct A < A(A&&) = delete; >;
то он будет ругаться, что такой конструктор удален. Конструктор копирования или оператор копирования при таком использовании так же игнорируются.
Получается, при удалении ругается, но при наличии его не использует. Как заставить его вызывать этот конструктор, что это такое и где про это прочитать?
В поисковике выдает выдает кучу примеров про этот конструктор, расписывая что в нем можно расположить какой-то код, но нигде не пишут что это код не вызывается.
Компилирую на GCC 9, на 8 вроде было так же.