🛠 Основы Move semantics в C++
В этой статье мы поговорим о том, что такое move semantics, зачем и когда она нужна, и как при помощи этого механизма оптимизировать программы на C++. Предполагается, что читатель знаком с концепцией ссылок в C++, классов, конструкторов, конструкторов копирования, переопределённых операторов и операторов копирования, а также правилом трёх. Примечание Move semantics была добавлена в C++ 11, следовательно, при использовании слишком старого компилятора данная фича будет недоступна. Каждое выражение в C++ характеризуется двумя свойствами: типом и категорией значения (value category [1]). В контексте разбора move semantics нас интересует только последнее. Полное описание категорий значений – тема для отдельной статьи, однако мы приведём необходимые сведения о каждой из существующих категорий значений. Стандарт языка определяет три основные категории значений и ещё две составные, которые определяются на основе первых трёх. Базовыми категориями значений являются lvalue, prvalue и xvalue: Определив три основные категории значений, можно определить две оставшиеся (составные) – glvalue и rvalue: Для ясности предлагаем взглянуть на диаграмму Венна: Примечание lvalue не обязательно всегда находится слева от знака равно, а rvalue – справа от него. Так было до введения move semantics в C++ 11. До C++ 11 мы имели лишь lvalue и rvalue, а после – rvalue разделили на два вида: xvalue и prvalue, в то время как совокупность xvalue и lvalue стали называть glvalue. Грубо говоря, lvalue – всё, чему может быть явно присвоено значение. rvalue – это временные объекты или значения, не связанные ни с какими объектами; что-то витающее в воздухе и ни за чем не закреплённое. Оставив самое сложное позади, поговорим о более близких к практике вещах, о ссылках на rvalue. При выполнении программы на C++ постоянно создаются и уничтожаются различного рода временные объекты (rvalue). До C++ 11 мы не имели возможности сохранить эти объекты для будущего использования, потому что не могли ссылаться на них (вернее, могли, но используя только константные ссылки, а значит, лишаясь возможности изменения). С приходом C++ 11 всё изменилось: появилась возможность ссылаться на rvalue (и изменять rvalue через эти ссылки) так же, как мы до этого ссылались на lvalue (кстати говоря, то, что в C++ мы обычно называем просто ссылками, является на самом деле ссылками на lvalue). Время для примера: Листинг 1 Примечание Время жизни rvalue, привязанного к ссылке на rvalue, расширяется до времени жизни этой ссылки. Важно понимать, что сама ссылка на rvalue является lvalue. Это всё очень хорошо, скажете вы, но как это поможет мне оптимизировать мои программы? Об этом – ниже. Добавим в наш класс X конструктор по умолчанию, конструктор и оператор копирования, а также объявление указателя на int. Листинг 2 Заменим Листинг 3 Примечание Предположим, что листинг 3 компилируется, как C++03, в котором move semantics ещё не существовало. Заметили, да? Мы копируем содержимое временного объекта, в то время как копирования фактически можно избежать, просто забрав (переместив) ресурс из временного объекта, т.к. этот объект всё равно очень скоро (после выхода из конструктора копирования) будет уничтожен и никто не пострадает, если его содержимое станет пустым (или не пустым, но невалидным). Это и есть move semantics. Важно понять, что move semantics не является способом увеличить производительность каждой строки вашего кода. Move semantics – это механизм, работающий только в определённых случаях. Несмотря на это, он может здорово повысить общую скорость работы вашей программы. Это всё звучит привлекательно, но как это реализовать? Очень просто, для этого нам понадобятся… С++ 11 дал нам два инструмента для реализации move semantics в пользовательских классах – конструктор перемещения и оператор перемещения. Это своего рода аналоги конструктора и оператора копирования, но предназначенные не для копирования, а для перемещения. Добавим их в наш класс Листинг 4 В конструкторе перемещения указатель на ресурс объекта, в который мы перемещаем, меняется на указатель на ресурс объекта, из которого мы перемещаем, и наоборот. То же самое происходит в операторе перемещения. В результате объект получает тяжеловесный ресурс, но при этом никакого копирования не происходит! Теперь при использовании компилятора, поддерживающего C++ 11, код из листинга 3 больше не будет вызывать оператор копирования, а вместо него будет вызывать оператор перемещения. Почему? Потому что в данном случае справа от знака равно находится rvalue, а конструктор и оператор копирования предназначены для работы именно с rvalue. Резюмируя последние четыре раздела статьи: Обратите внимание на то, что и конструктор и оператор копирования должны быть помечены как Стоит заметить, правило, известное как правило трёх, становится правилом пяти [9]: если вы реализуете в вашем классе один пункт из следующего списка, вы должны реализовать все пять: Примечание На самом деле, правильная реализация copy-and-swap idiom совмещает операторы копирования и перемещения [10]. Внимательный читатель наверняка задался вопросом, что делать, если класс содержит поля не примитивного типа (например, Круто… И что это нам даёт? Это даёт нам возможность перемещать объекты, rvalue-ссылок на которые у нас нет. Допустим, наш класс X имеет поле типа Листинг 5 Теперь в конструкторе перемещения для поля типа С точки зрения семантики, обёртка в Примечание* Копирования не произошло бы, потому что внутри Move semantics проявляется лишь в определённых случаях. Move semantics позволяет забирать ресурсы у временных объектов, которые, как правило, в скором времени будут уничтожены, тем самым избегая лишнего копирования. Основными инструментами языка и стандартной библиотеки для реализации move semantics являются: Конструктор перемещения вызывается, когда объект инициализируется rvalue, оператор перемещения – когда объекту присваивается rvalue. Не рассмотренными остались темы rvalue-ссылок в контексте C++ шаблонов, в частности, темы perfect forwarding и Что нужно знать перед прочтением этой статьи?
Что такое rvalue и lvalue
Ссылки на rvalue
#include <iostream> //Подопытный класс class X { public: void setA(double a) { //Какой-то сеттер } }; X someFunctionReturningX() { X x; return x; } int main() { // X& xLvalueRef = someFunctionReturningX(); //Не скомпилируется - нельзя привязать rvalue к ссылке на lvalue const X& xConstLvalueRef = someFunctionReturningX(); //xConstLvalueRef.setA(0); //Не скомпилируется X&& xRvalueRef = someFunctionReturningX(); //Привязывание временного объекта к ссылке на rvalue - объект можно менять xRvalueRef.setA(0); }
&&
), обозначающими объявление ссылки на rvalue [8]. Далее к этой ссылке привязывается rvalue, которое, как видно из 5-й строки, мы можем изменять.Что такое move semantics и когда она имеет место
X() { resource = new int[100]; } X(const X& x) { for (int i = 0; i < 100; ++i) resource[i] = x.resource[i]; } X& operator=(const X& x) { X copy(x); std::swap(resource, copy.resource); return *this; } ~X() { delete[] resource; } private: int* resource = nullptr;
resource
здесь – это какие-то данные, которые, с точки зрения производительности, тяжело и долго копируются и лишнего копирования которых стоит избегать.main
из листинга 1 на следующий:
int main() { X x; x = someFunctionReturningX(); //Вызов оператора копирования, внутри которого создаётся копия временного объекта }
Конструктор и оператор перемещения
X
:
X(X&& x) noexcept { std::swap(resource, copy.resource); } X& operator=(X&& x) noexcept { std::swap(resource, copy.resource); return *this; }
noexcept
.std::string
), не являющиеся указателями, ведь в таком случае при вызове std::swap
произойдет копирование*. Для таких ситуаций С++11 предлагает нам воспользоваться…std::move
std::move
[11] – это функция из стандартной библиотеки, определённая в хедере <utility>
, которая позволяет взять, что угодно (например, lvalue), и сделать из этого rvalue (xvalue, если быть точным).std::string
. Как реализовать конструктор и оператор перемещения правильно?
X(X&& x) : stringField(std::move(x.stringField)) noexcept { std::swap(resource, copy.resource); } X& operator=(X&& x) noexcept { stringField = std::move(x.stringField); std::swap(resource, copy.resource); return *this; }
std::string
(stringField
) вызывается конструктор перемещения класса std::string
, потому что вызов std::move
“сделал” из x.stringField
rvalue! В операторе перемещения для stringField
вызывается оператор перемещения std::string
, потому что вызов std::move
“сделал” из x.stringField
rvalue.std::move
позволяет отметить какой-либо объект как объект, чьи ресурсы могут быть перемещены. std::move
также активно используется в совокупности с умными указателями (std::unique_ptr
), о которых мы тоже писали.std::swap
тоже использует std::move
. Утешение Тема move semantics в C++ объективно непростая. Это нормально, если вы не всё поняли с первого раза. Перечитайте нашу статью, обратитесь к документации или другим статьям по этой теме и всё станет на свои места. Вывод
std::move
.std::move
отмечает объекты, ресурсы которых могут быть перемещены, превращая эти объекты в rvalue (xvalue).std::forward
[12], тема copy/move elision и RVO [13], а также тонкая возможность C++ – ref-qualified методы [14].
- 6 views
- 0 Comment
Свежие комментарии