Конструкторы 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.
Конструктор перемещения
Конструктор и оператор перемещения используются компилятором в разных ситуациях:
- конструктор перемещения применяется в местах, где объявление совпадает с определением (инициализацией) rvalue-ссылкой на экземпляр этого же класса, либо посредством direct initialization в конструкторе класса/структуры (если же определение произойдет с помощью lvalue-ссылки, то вызовется конструктор копирования);
- оператор перемещения применяется в местах, где экземпляр класса уже был ранее определен и к нему применяется operator = , который в качестве аргумента приминает rvalue-ссылку на экземпляр этого же класса (если же оператор принимает lvalue-ссылку , то вызовется оператор присваивания).
Про rvalue-ссылки можете почитать здесь, здесь и здесь.
Контрольный пример (для разъяснения отличия в работе данных конструкций)
#include class Buffer < public: Buffer(const std::string& buff) : pBuff(nullptr) , buffSize(buff.length()) < pBuff = new char[buffSize]; memcpy(pBuff, buff.c_str(), buffSize); >~Buffer() < destroy(); >Buffer(const Buffer& other) : pBuff(nullptr) , buffSize(other.buffSize) < pBuff = new char[buffSize]; memcpy(pBuff, other.pBuff, buffSize); >Buffer& operator=(const Buffer& other) < destroy(); buffSize = other.buffSize; pBuff = new char[buffSize]; memcpy(pBuff, other.pBuff, buffSize); return *this; >Buffer(Buffer&& tmp) : pBuff(tmp.pBuff) , buffSize(tmp.buffSize) < tmp.pBuff = nullptr; >Buffer& operator=(Buffer&& tmp) < destroy(); buffSize = tmp.buffSize; pBuff = tmp.pBuff; tmp.pBuff = nullptr; return *this; >private: void destroy() < if (pBuff) delete[] pBuff; >char* pBuff; size_t buffSize; >; Buffer CreateBuffer(const std::string& buff) < Buffer retBuff(buff); return retBuff; >int main() < Buffer buffer1 = CreateBuffer("123"); // срабатывает конструктор перемещения Buffer buffer2 = buffer1; // срабатывает конструктор копирования buffer2 = CreateBuffer("123"); // срабатывает конструктор перемещения, затем оператор перемещения buffer2 = buffer1; // срабатывает оператор присваивания >
Дополнение
В C++11 каждый класс, помимо конструктора по умолчанию, имеет следующие 5 дефолтных операций:
- конструктор копирования (copy constructor);
- оператор присваивания (copy assignment);
- конструктор перемещения (move constructor);
- оператор перемещения (move assignment);
- деструктор (destructor).
При определении одной из этих 5-ти операций рекомендуется явно указать (либо определить, либо объявить с помощью default или delete ) все остальные, т.к. все эти 5 операций тесно связаны. Это будет способствовать лучшему пониманию семантики класса при чтении кода.
Если явно определена одна из упомянутых 5-ти операций (в том числе с использованием default или delete ), то:
- недостающие операции копирования будут определены автоматически с поведением по умолчанию;
- недостающие операции перемещения определены не будут.
Это следует учитывать при написании классов.
Отслеживать
ответ дан 9 фев 2016 в 23:58
StateItPrimitive StateItPrimitive
7,759 1 1 золотой знак 24 24 серебряных знака 47 47 бронзовых знаков
Вопрос фактически звучит как «зачем вообще нужны конструкторы». Вопрос в принципе не относится к «перемещениям», а фактически сводится к принципиальной разнице между конструкторами (копирования, перемещения, и т.д.) и другими функциями-членами класса (операторы присваивания и т.д.)
Конструктор в общем случае работает на «сыром» (несконструированном, непроинициализорованном) блоке памяти. В момент начала работы конструктора объекта как такового еще не существует и он не имеет никакого предсказуемого состояния. Соответственно работа конструктора сводится к созданию/инициализации нового объекта в предоставленном блоке «сырой» памяти. Конструктор копирования, например, копирует это состояние из некоего объекта-образца, конструктор перемещения — перемещает, конструктор преобразования — преобразует и т.д. Конструктор перемещения никоим образом не выделяется из этого ряда.
Оператор присваивания же всегда имеет дело с уже проинициализированным/сконструированным объектом, находящимся в некоем предсказуемом «валидном» состоянии. Работа оператора присваивания сводится к освобождению исходного состояния объекта (освобождению ресурсов, например), за которым следует копирование (или перемещение, или преобразование и т.п.) нового состояния из некоего объекта-источника.
Вот собственно и все. Т.е. операторы присваивания в общем случае делают больше работы, чем конструкторы. Операторы присваивания уничтожают старое состояние объекта и создают новое. А конструкторам уничтожать нечего — они только создают новое состояние.
В рамках этой логики как конструктор перемещения, так и перемещающий оператор присваивания никак из общего ряда не выделяются. Поэтому не ясно, откуда вообще мог возникнуть вопрос вроде «Зачем нужен конструктор перемещения, если есть оператор перемещения?».
Отслеживать
ответ дан 10 фев 2016 в 1:49
AnT stands with Russia AnT stands with Russia
69.2k 3 3 золотых знака 62 62 серебряных знака 140 140 бронзовых знаков
В принципе, стандартная реализация оператора присваивания могла бы делать следующее: для объекта в левой части оператора присваивания вызвать сначала деструктор, а потом конструктор копирования, передав ему ссылку на объект в правой части. Наверное, это было бы лучше, чем просто копировать все поля из правого объекта в левый, но всё равно это было бы не совсем правильно. Дело в том, что при копировании объекта требуется выделить новый ресурс, но потенциально это может закончиться неудачей. Если это произойдёт, то после неудачной попытки присваивания объект в левой части останется в некорректном состоянии: старый ресурс уже освобождён (вызван деструктор), а новый захватить не получилось. Поэтому при перегрузке оператора присваивания нужно всегда сначала захватывать новый ресурс, и только если эта операция прошла успешно, освобождать старый, заменяя его новым (по этой причине оператор присваивания в принятом ответе написан не совсем правильно). В случае перемещения, в принципе, такой проблемы нет: копируется только ссылка на ресурс, и старый ресурс, которым владел объект в левой части оператора перемещения, может быть освобождён безболезненно, как до копирования ссылки, так и после. Скорее всего, аналогичное копированию разделение на конструктор и оператор в случае перемещения сделано для случая, когда неудачей закончилось освобождение ресурса в левой части оператора перемещения. В этой ситуации необходимо, чтобы объект в правой части продолжал владеть ресурсом. Таким образом сохраняется атомарность копирования и перемещения: либо нам удалось полностью создать копию объекта или полностью переместить ресурс из одного объекта в другой, либо копирование / перемещение прошло неудачно, и ни один из объектов не был изменён.
Отслеживать
ответ дан 18 ноя 2018 в 20:57
8,875 25 25 серебряных знаков 66 66 бронзовых знаков
- c++
- c++11
-
Важное на Мете
Связанные
Похожие
Подписаться на ленту
Лента вопроса
Для подписки на ленту скопируйте и вставьте эту ссылку в вашу программу для чтения RSS.
Дизайн сайта / логотип © 2024 Stack Exchange Inc; пользовательские материалы лицензированы в соответствии с CC BY-SA . rev 2024.1.3.2953
Нажимая «Принять все файлы cookie» вы соглашаетесь, что Stack Exchange может хранить файлы cookie на вашем устройстве и раскрывать информацию в соответствии с нашей Политикой в отношении файлов cookie.
c++: почему не вызывается конструктор перемещения?
Так этот код не вызывает конструктор перемещения или оператор перемещения, его вывод:
begin constructor by value 10 end destructor 10
При этом, если я напишу:
struct A < A(A&&) = delete; >;
то он будет ругаться, что такой конструктор удален. Конструктор копирования или оператор копирования при таком использовании так же игнорируются.
Получается, при удалении ругается, но при наличии его не использует. Как заставить его вызывать этот конструктор, что это такое и где про это прочитать?
В поисковике выдает выдает кучу примеров про этот конструктор, расписывая что в нем можно расположить какой-то код, но нигде не пишут что это код не вызывается.
Компилирую на GCC 9, на 8 вроде было так же.
Просто о сложном — 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 и т.п. А более опытным разработчикам - если дочитали до конца, буду рад услышать Ваши комментарии и замечания.