перепечатано из
http://stackoverflow.com/questions/3106110/what-are-move-semantics
Походу самый простой способ понять move семантику — это на примере кода. Давайте начнём с очень простого класса строк, который всего лишь хранит указатель на размещённый в куче блок памяти:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = strlen(p) + 1;
data = new char[size];
memcpy(data, p, size);
}
Так как мы решили самостоятельно управлять памятью, нам нужно следовать
Правилу трёх. Пока опустим оператор присваивания и напишем деструктор и конструктор копирования:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = strlen(that.data) + 1;
data = new char[size];
memcpy(data, that.data, size);
}
Параметр конструктора const string& подходит ко всем выражениям с типом string, что позволяет нам создавать копии в следующих примерах:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Терь наступает ключевой момент в понимании мув семантики. Обратите внимание, что только в первой строке, где мы копировали x, глубокая копия на самом деле необходима, потому, что мы можем использовать x позже, и будем сильно удивлены, если x как то изменится. Заметили как я только что 3 раза сказал x (четыре, если считать это предложение) и это каждый раз означало тот же объект? Мы называем такие выражения, как x "lvalue"
Аргументы в строках 2 и 3 это не lvalue, а rvalue, поскольку эти строковые объекты не имеют имён, и вызывающий код не имеет возможности дальнейшего их использования. rvalue это временные объекты, которые уничтожаются в конце выражения. Это важно, поскольку во время инициализации b и c мы можем делать что угодно с исходными строками и вызывающий код об этом никак не узнает.
C++0x предаставляет новый механизм называющийся "rvalue reference" который помимо прочего, через перегрузку функций, позволяет нам определять что аргумент является rvalue. Всё что, нам для этого надо — это написать конструктор с параметром
ссылкой на rvalue. Внутри этого конструктора мы можем делать всё что угодно с исходным значением, пока оно остаётся в каком то валидном состоянии
string(string&& that) // string&& это rvalue ссылка на string
{
data = that.data;
that.data = 0;
}
Ну и что мы тут сделали? Вместо глубокой копии данных из кучи мы просто скопировали указатели и установили оригинальный указатель в null. В результате мы "стащили" данные, которые изначально принадлежали исходной строке. Опять же ключевой момент в том что вызывающий код ни при каких обстоятельствах не узнает, что исходный объект был изменён. Поскольку на самом деле мы не создавали копию, такой конструктор называется "move constructor". Его работа заключается в том чтобы, вместо копирования, просто переместить ресурсы из одного объекта в другой.
Поздравляю, вы только что поняли основы move семантики. Давайте продолжим реализовав оператора присваивания. Если вы пока не знакомы с
copy and swap идиомой то почитайте и возвращайтесь назад, поскольку это довольно грамотный подход, для того чтобы обеспечивать безопасность исключений в c++
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
Опа. Ну и чё это? Где здесь "rvalue reference"? кто то спросит. А я отвечу "А оно нам тут и не надо )"
Заметьте мы тут передали параметр по значению, так чтобы он инициализировался как любой другой строковый объект. Ну и как он будет инициализироваться? Раньше, во времена C++98, ответ был бы "с помощью конструктора копирования". В C++0x компилятор уже выбирает между конструктором копирования и конструктором переноса на основе того является аргумент lvalue или rvalue.
Так, если вы напишете a = b "that" будет инициализировано конструктором копирования (по тому, что b это lvalue) и оператор присваивания обменяется содержимым с только что созданной копией. Вот и всё определение идиомы copy and swap — создать копию, обменяться содержимым с копией, а потом избавиться от копии выходом из области видимости. Здесь больше ничего нового.
Но если вы напишете a = x + y "that" будет инициализировано с помощью конструктора переноса (т.к. x + y это rvalue), таким образом глубокая копия не создаётся, только переходит владение. "that" всё ещё независимый от аргументов объект, но его создание было дешевым, так как данные из кучи не копировались, а только меняли владельца.
Итого: Конструктор копирования создаёт глубокую копию, для того чтобы исходные данные оставались нетронутыми. Конструктор перемещения может копировать только указатель, и после этого установить указатель и исходном объекте в null. Занулять указатель в исходном объекте надо, иначе данные будут удалены слишком рано — в деструкторе временного объекта, да ещё и второй раз — в деструкторе объекта, куда мы их перетащили.
Ну и слегка поподробнее:
Введение
Семантика премещения позволяет, при определенных условиях, получать во владение ресурсы какого либо другого объекта. Это важно в двух случаях:
1. Замена дорогого копирования дешевой сменой владельца. Обратите внимание, что если объект не управляет по крайней мере одним внешним ресурсом (непосредственно, или через другой объект его член) семантика копирования не представляет никакого преимущества перед семантикой копирования. В этом случае копирование и перемещение — одно и то же:
class cannot_benefit_from_move_semantics
{
int a; // перемещение инта тоже самое что и копирование инта
char d[64]; // перемещение массива символов тоже самое что и копирование массива символов
// ...
};
2. Реализация безопасных "move-only" типов; это типы для которых копирование не имеет смысла а перемещение имеет. Например файловые хендлеры, смартпоинтеры с семантикой уникального владения.
Что значит перемещение?
Стандартная библиотека С++98 предоставляет смартпоинтер с семантикой уникального владения std::auto_ptr<T>, который гарантирует что динамически размещённый объект будет удалён даже в случае исключения:
{
std::auto_ptr<Shape> a(new Triangle);
// ...
// arbitrary code, could throw exceptions
// ...
} // <--- when a goes out of scope, the triangle is deleted automatically
необычность auto_ptr состоит в его "копирующем" поведении:
auto_ptr<Shape> a(new Triangle);
+---------------+
| triangle data |
+---------------+
^
|
|
|
+-----|---+
| +-|-+ |
a | p | | | |
| +---+ |
+---------+
auto_ptr<Shape> b(a);
+---------------+
| triangle data |
+---------------+
^
|
+----------------------+
|
+---------+ +-----|---+
| +---+ | | +-|-+ |
a | p | | | b | p | | | |
| +---+ | | +---+ |
+---------+ +---------+
Заметьте, что при инициализации b переменной a треугольник не копируется, вместо этого передаётся владение треугольником из a в b. Как ещё говорят "a переместилось в b" или "треугольник переместился из a в b". Это может звучать странно, поскольку треугольник сам по себе оставался на том же месте в памяти.
Переместить объект означает передать владение какого либо ресурса, которым он управлял, в другой объект.
Конструктор копирования auto_ptr скорее всего выглядит как то так (немного упрощённо):
auto_ptr(auto_ptr& source) // Обратите внимание нету const
{
p = source.p;
source.p = 0; // терь source больше не владеет объектом
}
Dangerous and harmless moves
Опасная фигня с auto_ptr заключается в том, что синтаксически это выглядит что копия на самом деле переместилась.
Попытка вызова функции-члена auto_ptr из которого перемещены данные приводит к неопределенному поведению, поэтому надо быть очень осторожным чтобы не использовать auto_ptr после того как из него произошло перемещение:
auto_ptr<Shape> a(new Triangle); // создаём треугольник
auto_ptr<Shape> b(a); // перемещаем из a в b
double area = a->area(); // неопределенное поведение
But auto_ptr is not always dangerous. Factory functions are a perfectly fine use case for auto_ptr:
Но auto_ptr не всегда опасны. Фабричные функции — отличный юзкейс для auto_ptr:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // перемещаем временный объект в c
double area = make_triangle()->area(); // всё безопасно
Обратите внимание, как оба примера следуют одной и той же семантике:
auto_ptr<Shape> variable(expression);
double area = expression->area();
И в то время как один из них вызывает неопределенное поведение другой нет. Так какая разница между expressions и make_triangle()? Они что, разного типа? Не, одного. Но они разных value категорий.
Value категории
Очевидно должно быть глубокое различие между переменной auto_ptr expression и выражением make_triangle(), которое является вызовом функции, возвращающей auto_ptr по значению, которая создаёт новый временный auto_ptr объект каждый раз, когда она вызывается. expression это пример lvalue, в то время как make_triangle() — пример rvalue.
Перемещение из lvalue как в пример выше опасно, потому, что мы можем позже вызвать функцию-член через a, что приведёт к неопределённому поведению. С другой стороны перемещение из rvalue совершенно безопасно потому, что после того как конструктор копирования завершил свою работу мы не можем снова использовать временный объект. Если мы просто напишем make_triangle() ещё раз, мы получим новый временный объект. Фактически временный объект, из которого производится перемещение уничтожается на следующей строке
auto_ptr<Shape> c(make_triangle());
^ временный объект, из которого производится перемещение умирает прямо тут
Обратите внимание, что буквы l и r исторически произошли от левой(left-hand) и правой(right-hand) стороны присваивания.
В С++ это больше не так потому, что существуют lvalue, которые не могут появиться с левой стороны присваивания (навроде массивов и типов определённых пользователем без оператора присваивания) и rvalue которые могут
Ссылки на rvalue
Теперь мы понимаем, что перемещение из lvalue потенциально опасно, а перемещение из rvalue нет. Если бы язык С++ давал возможность отличить lvalue аргументы от rvalue аргументов мы можем либо полностью запретить перемещение из lvaluе, либо по крайней мере сделать перемещение из lvalue явным в месте вызова, и таким образом мы избегаем случайного перемещения.
С++11 решает эту проблему ссылками на rvalue. rvalue ссылки это новый вид ссылок, которые связываются только с rvalue объектами, и имеют синтаксис X&&. Старые добрые X& ссылки теперь называются lvalue ссылками. (Заметьте что X&& это не ссылки на ссылки, такой штуки в С++ нету)
Если мы добавим ещё и const, то получим уже четыре различных типа ссылок. Тут перечислены виды выражений типа X, с которыми они могут быть связаны (bind).
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& да
const X& да да да да
X&& да
const X&& да да
На практике вы можете забыть про X&&. От rvalue только для чтения мало толку.
rvalue ссылки X&& это новый вид ссылок, которые привязываются (binds) только rvalue.
Неявные преобразования
rvalue ссылки прошли через нескольких версий. С версии 2.1 ссылки X&& также связываются со всеми value категориями отличного типа Y, если предоставлен способ неявного преобразования из Y в X. В этом случае создаётся временный объект типа X и rvalue ссылка связывается с этим временным объектом:
void some_function(std::string&& r);
some_function("hello world");
В примере, приведенном выше, "hello world" это lvalue имеющее тип const char[12]. Так как тут неявное преобразование из const char[12] через const char* в std::string, создаётся временный объект типа std::string, и r с ним связывается. Это один из случаев, когда разница между rvalues (выражениями) и временными объектами немного размыта.
Конструкторы перемещения
Полезный пример функции с параметром X&& это конструктор перемещения X::X(X&& source. Он предназначен для передачи владения ресурсом из объекта source в текущий объект.
В С++11 std::auto_ptr<T> заменен на std::unique_ptr<T>, который имеет преимущество rvalue ссылок. Мы тут создадим и обсудим упрощенную версию unique_ptr.
Сперва мы инкапсулируем сырой указатель и перегрузим операторы -> и *, так чтобы наш класс вёл себя как указатель:
template<typename T>
class unique_ptr
{
T* ptr;
public:
T* operator->() const
{
return ptr;
}
T& operator*() const
{
return *ptr;
}
Конструктор получает объект во владение, а деструктор его удаляет:
explicit unique_ptr(T* p = nullptr)
{
ptr = p;
}
~unique_ptr()
{
delete ptr;
}
Теперь интересная часть — Конструктор перемещения:
unique_ptr(unique_ptr&& source) // rvalue ссылка
{
ptr = source.ptr;
source.ptr = nullptr;
}
Этот конструктор перемещения работает так же как и конструктор копирования auto_ptr, но он применим только к rvalue:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Вторая строчка не компилируется потому что a это lvalue, а параметр unique_ptr&& принимает только rvalue. Это именно то, чего мы хотели. Опасное перемещение никогда больше не произойдёт неявно. Третья строка компилируется без проблем, так как make_triangle() это rvalue. Конструктор перемещения преренесёт владение от временного объекта в c. Опять же это то что нам нужно.
Перемещающий конструктор передаёт владение управляемым ресурсом в текущий объект
Перемещающий оператор присваивания
Последняя недостающая часть это Перемещающий оператор присваивания. Его задача состоит в освобождении старого ресурса и захвате нового ресурса из аргумента:
unique_ptr& operator=(unique_ptr&& source) // rvalue ссылка
{
if (this != &source) // не присваиваем самому себе
{
delete ptr; // освобождаем старый
ptr = source.ptr; // захватываем новый
source.ptr = nullptr;
}
return *this;
}
};
Тут реализация перемещающего оператора присваивания дублирует логику деструктора и перемещающего конструктора.
Помните про copy-and-swap идиому? Она также может быть применена к семантике перемещения как move-and-swap идиома
unique_ptr& operator=(unique_ptr source) // здесь нету ссылки
{
std::swap(ptr, source.ptr);
return *this;
}
};
Теперь source это переменная типа unique_ptr, она должна быть инициализирована конструктором перемещения; таким образом аргумент будет перемещен в параметр. Всё ещё требуется чтобы аргумент был rvalue, потому что конструктор перемещения сам по себе имеет параметр ссылку на rvalue. Когда порядок выполнения достигает закрывающей фигульной скобки оператора=, source выходит из области видимости и уничтожается, автоматически освобождая старый ресурс.
Перемещающий оператор присваивания передаёт владение управляемым ресурсом в текущий объект и освобождает старый ресурс. Идиома move-and-swap облегчает реализацию.
Перемещение из lvalue
Иногда нам хотелось бы переместить из lvalue. То есть мы хотели бы чтобы компилятор обращался с lvalue как будто это rvalue, так чтоб он мог вызвать конструктор перемещения, пусть это было бы потенциально небезопасно. Для этих целей С++11 предлагает функцию стандартной библиотеки std::move, которая находится в заголовке <utility>. Имя выбрано слегка неудачно, потому что std::move всего лишь приводит lvalue к rvalue, и ничего никуда не перемещает. Эта функция всего лишь делает возможным перемещение. Возможно она должна была называться std::cast_to_rvalue или std::enable_move, но в настоящее время название уже устаканилось.
Тут показано как вы можете перемещать из lvalue явно:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // всё ещё ошибка
unique_ptr<Shape> c(std::move(a)); // okay
Обратите внимание, что после третьей строки a больше не владеет треугольником. Это нормально, поскольку явно написав std::move(a) мы сделали ясными наши намерения: "Уважаемый конструктор, делай что захочешь с a, чтобы инициализировать c; Меня а больше не интересует."
std::move(some_lvalue) приводит lvalue к rvalue, и делает возможным дальнейшее перемещение.
Xvalue
Обратите внимание, что хотя std::move(a) и rvalue, в результате не создаётся временный объект. Эта проблема вынудила комитет ввести третью категорию value. Это что то, что может быть связано с rvalue ссылкой, хотя оно и не rvalue в традиционном понимании, и назвали это xvalue (eXpiring value). А традиционное rvalue было переименовано в prvalue(Pure rvalues).
Оба prvalue и xvalue являются rvalues. Xvalue и lvalue оба являются glvalues (Generalized lvalues). Взаимотношение между ними проще всего можно представить на диаграмме:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Заметьте, что только xvalues является нововведением; остальные появились в результате переименования и группировки.
rvalues из C++98 в C++11 известны как prvalues. Мысленно замените все "rvalue" из предыдущих параграфов на "prvalue".
Перемещение из функций
До сих пор мы наблюдали только перемещение локальных переменных в параметры функций. Но перемещение так же возможно в обратном направлении. Если функция возвращает по значению, какой то объект в месте вызова (скорее всего локальный или временный, но может быть любой тип объекта) инициализируется выражением после оператора return как аргументом в конструкторе перемещения:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| временный объект перемещается в c
|
v
unique_ptr<Shape> c(make_triangle());
Возможно это покажется странным, автоматические объекты (локальные переменные, которые не были объявлены как static) так же могут быть перемещены из функций:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // std::move не используем
}
}
Как так получилось что конструктор перемещения принимает lvalue в качестве аргумента? Область видимости переменной result заканчивается и она должна будет уничтожен во время разворачивания стека. Когда управление возвращается в место вызова переменной result больше не существует. По этой причине С++11 имеет специальное правило, которое позволяет возвращать автоматические объекты из функций без вызова std::move. Вообще вы никогда не должны использовать std::move для перемещения автоматических объектов из функций, так как это препятствует оптимизации NRVO "named return value optimization"
Никогда не используйте std::move, чтобы переместить из функции.
Обратите внимание, что в обоих фабричных функциях возвращаемый тип это значение(value), не ссылка на rvalue. Rvalue ссылки — всё ещё остаются ссылками, и как обычно, вы никогда не должны возвращать ссылки на автоматические объекты. Вызывающая сторона будет иметь дело с висящими ссылками, если вы попытаетесь сделать так:
unique_ptr<Shape>&& flawed_attempt() // НЕ ДЕЛАЙТЕ ТАК!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // НЕВЕРНО!
}
Никогда не возвращайте автоматические объекты по rvalue ссылке. Перемещение производится исключительно перемещающим конструктором, не вызовом std::move и не только лишь привязкой rvalue к ссылок на rvalue.
Перемещение в члены класса
Рано или поздно вы попытаетесь написать код навроде этого:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // ошибочка
{}
};
Обычно компилятор пожалуется на то что параметр lvalue. Если вы посмотрите на этот тип вы увидите ссылку на rvalue, но rvalue ссылка просто означает "ссылку, которая привязывается к rvalue"; Это не означает что сама ссылка является rvalue! В самом деле parameter это просто обычная переменная с именем. Вы можете использовать parameter столько раз, сколько вам нужно внутри тела конструктора, и это всегда будет относится к одному и тому же объекту. Неявное перемещение из него будет опасным, по этому оно запрещено языком.
Именованная rvalue ссылка является lvalue, как и любая другая переменная.
Решение в том, чтобы вручную разрешить перемещение:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(std::move(parameter)) // обратите внимание на std::move
{}
};
Вы можете возразить, что parameter больше не используется после инициализации члена. Почему тут нет специального правила пропускать вставку std::move как было с возвращаемым значением? Возможно потому, что это было бы слишком сложно для реализации компилятора. Например сто если тело конструктора в другой единице трансляции? В отличие от этого в возвращаемом значении гораздо проще по таблице символов определить есть ли идетнификаторы после оператора return, относящиеся к автоматическому объекту.
Вы также можете передать параметр по значению. Для move-only типов, таких как unique_ptr похоже пока не существует устоявшейся идиомы. Я предпочитаю передавать по значению, так как уменьшает количество путаницы в интерфейсе.
Специальные функции-члены
C++98 неявно определяет три специальных функции-члена: конструктор копирования, оператор присваивания и деструктор.
X::X(const X&); // конструктор копирования
X& X::operator=(const X&); // копирующий оператор присваивания
X::~X(); // деструктор
Как мы знаем rvalue ссылки прошли через нескольких версий. С версии 3.0 C++11 добавляет две дополнительных специальных функции-члена: конструктор перемещения и перемещающий оператор присваивания. Заметьте, что ни VC10 ни VC11 пока не поддерживают 3.0, поэтому мы будем реализовывать их самостоятельно.
X::X(X&&); // конструктор перемещения
X& X::operator=(X&&); // перемещающий оператор присваивания
эти две специальные функции объявляются неявно только если они не объявлены вручную. И ещё если вы определили свои собственные перемещающие конструктор или оператор присваивания неявные копирующие конструктор и оператор присваивания не объявляются.
Что эти правила означают на практике?
Если вы напишете класс без ресурсов, которыми нужно управлять, тогда нет необходимости в какой либо из пяти специальных функций, и у вас будет корректная семантика копирования и перемещения без вашего вмешательства. Иначе вам нужно реализовывать спецфункции самостоятельно. И конечно же если ваш класс не получает преимуществ от семантики перемещения, то нет необходимости в реализации специальных операций перемещения.
Заметьте, что копирующий оператор присваивания и перемещающий оператор присваивания могут быть объединены в один унифицированный оператор присваивания, принимающий аргумент по значению:
X& X::operator=(X source) // унифицированный оператор присваивания
{
swap(source);
return *this;
}
Таким образом число спецфункций уменьшается до пяти.
Универсальные ссылки
Посмотрите на следующую шаблонную функцию:
template<typename T>
void foo(T&&);
Вы можете ожидать что T&& привязывается только к rvalues, потому, что на первый взгляд это выглядит как ссылка на rvalue. Как оказывается T&& также связывается с lvalue
foo(make_triangle()); // T является unique_ptr<Shape>, T&& является unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T является unique_ptr<Shape>&, T&& является unique_ptr<Shape>&
Если аргумент это rvalue типа X, T становится типом X, и тут T&& означает X&&. Это ожидаемо. Но если аргумент lvalue типа X, благодаря специальному правилу T становится типом X&, отсюда T&& будет означать что то вроде X& &&. Но так как ссылок на ссылки в C++ нет тип X& && сокращается до X&
По началу это может звучать сбивающим с толку, такое сокращение ссылок предназначено для обеспечения возможности идеальной передачи(perfect forwarding)
T&& это не rvalue ссылка, а универсальная ссылка. Она также привязывается к lvalue, в этом случае T и T&& оба ссылки на lvalue.
Реализация перемещения
Теперь, когда вы понимаете сокращение ссылок (reference collapsing), посмотрим как реализована std::move
template<typename T>
typename std::remove_reference<T>::type&&
move(T&& t)
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
Как вы видите move принимает параметр любого типа, благодаря универсальной ссылке T&& и возвращает ссылку на rvalue. Вызов мета-функции std::remove_reference<T>::type необходим, иначе для lvalue типа X, возвращаемый тип будет X& &&, который сократится до X&. Так как это всегда lvalue (вспомните, что именованная rvalue ссылка является lvalue) но мы хотим привязать t к ссылке на rvalue, мы явно приводим t к корректному возвращаемому типу. Вызов функции, возвращающей ссылку на rvalue сам по себе является xvalue.
Вызов функции, возвращающей ссылку на rvalue такой, как td::move является xvalue.
Повторяю еще раз — на уровне метафор все понятно.
Не хватает пока конкретного ПРОСТОГО примера для показа учням.
Но вроде потихоньку вырисовывается.
Поднял еще раз Стенли Липпмана — там МНОГО места посвящено проблемам swap().
Именно то, что нужно.