Что такое семантика перемещения?
Я только что закончил слушать радио- интервью подкаста Software Engineering со Скоттом Мейерсом о С++ 0x. Большинство новых функций имело смысл для меня, и сейчас я на самом деле рад С++ 0x, за исключением одного. Я до сих пор не понимаю семантику перемещения... Что это такое?
Ответы
Ответ 1
Мне проще понять семантику перемещения с помощью примера кода. Начнем с очень простого строкового класса, который содержит только указатель на выделенный в куче блок памяти:
#include <cstring>
#include <algorithm>
class string
{
char* data;
public:
string(const char* p)
{
size_t size = std::strlen(p) + 1;
data = new char[size];
std::memcpy(data, p, size);
}
Поскольку мы решили сами управлять памятью, нам нужно следовать правилу трех. Я собираюсь отложить написание оператора присваивания и пока реализовать только деструктор и конструктор копирования:
~string()
{
delete[] data;
}
string(const string& that)
{
size_t size = std::strlen(that.data) + 1;
data = new char[size];
std::memcpy(data, that.data, size);
}
Конструктор копирования определяет, что значит копировать строковые объекты. Параметр const string& that
связывается со всеми выражениями типа string, позволяет копировать в следующих примерах:
string a(x); // Line 1
string b(x + y); // Line 2
string c(some_function_returning_a_string()); // Line 3
Теперь приходит ключ к пониманию семантики перемещения. Обратите внимание, что только в первой строке, где мы копируем x
, эта глубокая копия действительно необходима, потому что мы могли бы захотеть проверить x
позже и были бы очень удивлены, если x
изменилось каким-либо образом. Вы заметили, как я только что сказал x
три раза (четыре раза, если вы включите это предложение) и имел в виду один и тот же объект каждый раз? Мы называем такие выражения, как x
"lvalues".
Аргументы в строках 2 и 3 - это не lvalues, а rvalues, потому что нижележащие строковые объекты не имеют имен, поэтому у клиента нет возможности проверить их снова в более поздний момент времени. Значения r обозначают временные объекты, которые уничтожаются в следующей точке с запятой (точнее: в конце полного выражения, которое лексически содержит значение r). Это важно, потому что во время инициализации b
и c
мы могли делать с исходной строкой все, что хотели, и клиент не мог сказать разницу!
C++ 0x представляет новый механизм, называемый rvalue reference, который, помимо прочего, позволяет обнаруживать rvalue аргументы через перегрузку функций. Все, что нам нужно сделать, это написать конструктор со ссылочным параметром rvalue. Внутри этого конструктора мы можем делать с источником все, что захотим, при условии, что мы оставляем его в каком-то допустимом состоянии:
string(string&& that) // string&& is an rvalue reference to a string
{
data = that.data;
that.data = nullptr;
}
Что мы здесь сделали? Вместо глубокого копирования данных кучи, мы просто скопировали указатель, а затем установили исходный указатель на null (чтобы не допустить "delete []" из деструктора исходного объекта для освобождения наших "только что украденных данных"). По сути, мы "украли" данные, которые изначально принадлежали исходной строке. Опять же, ключевой момент заключается в том, что ни при каких обстоятельствах клиент не может обнаружить, что источник был изменен. Поскольку мы на самом деле не делаем здесь копию, мы называем этот конструктор "конструктором перемещения". Его задача - перемещать ресурсы с одного объекта на другой, а не копировать их.
Поздравляем, теперь вы понимаете основы семантики перемещения! Давайте продолжим, реализовав оператор присваивания. Если вы не знакомы с идиомой копирования и обмена, изучите ее и возвращайтесь, потому что это замечательная идиома C++, связанная с безопасностью исключений.
string& operator=(string that)
{
std::swap(data, that.data);
return *this;
}
};
А это что? "Где ссылка?" Вы можете спросить. "Нам здесь не нужно!" мой ответ :)
Обратите внимание, что мы передаем параметр, that
по значению, так that
должен быть инициализирован так же, как и любой другой объект строки. Как именно that
будет инициализировано? В былые времена C++ 98 ответом был бы "конструктор копирования". В C++ 0x компилятор выбирает между конструктором копирования и конструктором перемещения в зависимости от того, является ли аргумент оператора присваивания lvalue или rvalue.
Поэтому, если вы скажете a = b
, конструктор копирования инициализирует that
(поскольку выражение b
является l-значением), а оператор присваивания заменяет содержимое только что созданной глубокой копией. Это само определение копии и идиома замены - создайте копию, обменяйте содержимое копией, а затем избавьтесь от копии, покинув область действия. Здесь нет ничего нового.
Но если вы скажете a = x + y
, конструктор перемещения инициализирует that
(потому что выражение x + y
является значением r), поэтому глубокая копия не используется, только эффективный ход. that
все еще независимый объект от аргумента, но его конструкция была тривиальной, так как данные кучи не нужно было копировать, просто перемещать. Не было необходимости копировать его, потому что x + y
- это r-значение, и, опять же, можно перейти от строковых объектов, обозначенных как r-значения.
Подводя итог, конструктор копирования делает глубокую копию, потому что источник должен оставаться нетронутым. С другой стороны, конструктор перемещения может просто скопировать указатель, а затем установить нулевой указатель в источнике. Можно "обнулить" исходный объект таким образом, потому что у клиента нет способа снова проверить объект.
Я надеюсь, что этот пример объяснил главное. Существует намного больше, чтобы ценить ссылки и перемещать семантику, которую я намеренно оставил для простоты. Если вы хотите получить более подробную информацию, пожалуйста, смотрите мой дополнительный ответ.
Ответ 2
Моим первым ответом было чрезвычайно упрощенное введение в перенос семантики, и многие детали были упущены, чтобы сохранить его просто. Однако есть намного больше, чтобы переместить семантику, и я подумал, что пришло время для второго ответа, чтобы заполнить пробелы. Первый ответ уже довольно старый, и ему не хотелось просто заменить его совершенно другим текстом. Я думаю, что это все еще хорошо, как первое введение. Но если вы хотите копать глубже, читайте дальше :)
Стефан Т. Лававей занял время, предоставляя ценные отзывы. Большое спасибо, Стефан!
Введение
Перемещение семантики позволяет объекту при определенных условиях владеть другими внешними ресурсами объекта. Это важно двумя способами:
-
Превращение дорогих копий в дешевые ходы. См. Мой первый ответ для примера. Обратите внимание: если объект не управляет хотя бы одним внешним ресурсом (прямо или косвенно через его объекты-члены), перемещение семантики не будет иметь никаких преимуществ перед семантикой копирования. В этом случае копирование объекта и перемещение объекта означает то же самое:
class cannot_benefit_from_move_semantics
{
int a; // moving an int means copying an int
float b; // moving a float means copying a float
double c; // moving a double means copying a double
char d[64]; // moving a char array means copying a char array
// ...
};
-
Внедрение безопасных типов "только для перемещения"; то есть типы, для которых копирование не имеет смысла, но перемещение происходит. Примеры включают блокировки, дескрипторы файлов и интеллектуальные указатели с уникальной семантикой собственности. Примечание. В этом ответе обсуждается std::auto_ptr
, устаревший стандартный шаблон библиотеки C++ 98, который был заменен на std::unique_ptr
в C++ 11. Промежуточные программисты C++, вероятно, по крайней мере знакомы с std::auto_ptr
, и из-за "семантики перемещения", которую он отображает, это кажется хорошей отправной точкой для обсуждения семантики перемещения в C++ 11. YMMV.
Что такое движение?
Стандартная библиотека C++ 98 предлагает интеллектуальный указатель с уникальной семантикой собственности, называемый std::auto_ptr<T>
. Если вы не знакомы с auto_ptr
, его цель состоит в том, чтобы гарантировать, что динамически выделенный объект всегда будет выпущен, даже перед исключениями:
{
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) // note the missing const
{
p = source.p;
source.p = 0; // now the source no longer owns the object
}
Опасные и безобидные движения
auto_ptr
особенностью auto_ptr
является то, что синтаксически выглядит как копия на самом деле. При попытке вызвать функцию-член в перемещенном из auto_ptr
вызовется неопределенное поведение, поэтому вы должны быть очень осторожны, чтобы не использовать auto_ptr
после его перемещения из:
auto_ptr<Shape> a(new Triangle); // create triangle
auto_ptr<Shape> b(a); // move a into b
double area = a->area(); // undefined behavior
Но auto_ptr
не всегда опасен. Фабричные функции - прекрасный способ использования auto_ptr
:
auto_ptr<Shape> make_triangle()
{
return auto_ptr<Shape>(new Triangle);
}
auto_ptr<Shape> c(make_triangle()); // move temporary into c
double area = make_triangle()->area(); // perfectly safe
Обратите внимание, что оба примера соответствуют одному и тому же синтаксическому шаблону:
auto_ptr<Shape> variable(expression);
double area = expression->area();
И все же один из них вызывает неопределенное поведение, тогда как другой - нет. Так в чем же разница между выражениями a
и make_triangle()
? Разве они не одного типа? Действительно, они есть, но у них разные категории значений.
Категории значений
Очевидно, что должно существовать некоторая глубокая разница между выражением a
которое обозначает переменную auto_ptr
, и выражение make_triangle()
которое обозначает вызов функции, которая возвращает значение auto_ptr
по значению, тем самым создавая новый временный объект auto_ptr
каждый раз, когда он вызывается, a
- пример lvalue, тогда как make_triangle()
является примером rvalue.
Перемещение с lvalues, таких как a
является опасным, потому что мы могли бы позже попытаться вызвать функцию-член через a
, вызывая неопределенное поведение. С другой стороны, переход от rvalues, таких как make_triangle()
, совершенно безопасен, потому что после того, как конструктор копирования выполнил свою работу, мы не сможем использовать временное снова. Нет выражения, которое обозначает указанное временное; если мы просто снова напишем make_triangle()
, мы получим другое временное. Фактически, перемещенная временная часть уже перешла на следующую строку:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Обратите внимание, что буквы l
и r
имеют историческое происхождение в левой и правой частях задания. Это больше не верно в C++, потому что есть lvalues, которые не могут появиться в левой части присваивания (например, массивы или пользовательские типы без оператора присваивания), и есть rvalues, которые могут (все значения r типы классов с оператором присваивания).
Rvalue типа класса - выражение, оценка которого создает временный объект. При нормальных обстоятельствах никакое другое выражение внутри одного и того же объекта не указывает на тот же временный объект.
Ссылки Rvalue
Теперь мы понимаем, что переход от lvalues потенциально опасен, но переход от rvalues безвреден. Если C++ имеет языковую поддержку для различения аргументов lvalue из аргументов rvalue, мы можем либо полностью запретить переход от lvalues, либо, по крайней мере, сделать переход от lvalues явным на сайте вызова, чтобы мы больше не двигались случайно.
C++ 11 Ответ на эту проблему - ссылки rvalue. Ссылка rvalue - это новый тип ссылок, который привязывается только к rvalues, а синтаксис - X&&
. Хорошая старая ссылка X&
теперь известна как ссылка lvalue. (Обратите внимание, что X&&
не является ссылкой на ссылку, такого нет в C++.)
Если мы добавим const
в микс, у нас уже есть четыре разных типа ссылок. С какими выражениями типа X
они могут связываться?
lvalue const lvalue rvalue const rvalue
---------------------------------------------------------
X& yes
const X& yes yes yes yes
X&& yes
const X&& yes yes
На практике вы можете забыть о const X&&
. Ограничение чтения из rvalues не очень полезно.
Ссылка на rvalue X&&
- это новый тип ссылок, который привязывается только к значениям r.
Неявные преобразования
Ссылки Rvalue прошли через несколько версий. Начиная с версии 2.1, ссылка на rvalue X&&
также связывается со всеми категориями значений другого типа 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
типа std::string
и r
привязана к этому временному. Это один из случаев, когда различие между rvalues (выражениями) и временными (объектами) немного размыто.
Переместить конструкторы
Полезным примером функции с параметром X&&
является конструктор перемещения X::X(X&& source)
. Его целью является передача права собственности на управляемый ресурс из источника в текущий объект.
В C++ 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) // note the rvalue reference
{
ptr = source.ptr;
source.ptr = nullptr;
}
Этот конструктор перемещения делает именно то, что сделал конструктор копии auto_ptr
, но он может быть предоставлен только с rvalues:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // error
unique_ptr<Shape> c(make_triangle()); // okay
Вторая строка не скомпилируется, поскольку a
является значением lvalue, но параметр unique_ptr&& source
может быть привязан только к значениям r. Это именно то, что мы хотели; опасные движения никогда не должны быть скрытыми. Третья строка компилируется просто отлично, потому что make_triangle()
является rvalue. Конструктор перемещения перенесет право собственности с временного на c
. Опять же, это именно то, что мы хотели.
Конструктор перемещения передает право собственности на управляемый ресурс в текущий объект.
Перемещение операторов назначения
Последний недостающий элемент - оператор присваивания перемещения. Его задача - освободить старый ресурс и получить новый ресурс из его аргумента:
unique_ptr& operator=(unique_ptr&& source) // note the rvalue reference
{
if (this != &source) // beware of self-assignment
{
delete ptr; // release the old resource
ptr = source.ptr; // acquire the new resource
source.ptr = nullptr;
}
return *this;
}
};
Обратите внимание, как эта реализация оператора присваивания перемещения дублирует логику как деструктора, так и конструктора перемещения. Вы знакомы с идиомой копирования и свопинга? Он также может применяться для перемещения семантики в качестве идиомы "перемещение и своп":
unique_ptr& operator=(unique_ptr source) // note the missing reference
{
std::swap(ptr, source.ptr);
return *this;
}
};
Теперь этот source
является переменной типа unique_ptr
, он будет инициализирован конструктором перемещения; то есть аргумент будет перенесен в параметр. Аргумент по-прежнему должен быть значением rvalue, потому что сам конструктор перемещения имеет параметр ссылки rvalue. Когда поток управления достигает закрывающей скобки operator=
, source
выходит за пределы области действия, автоматически отпуская старый ресурс.
Оператор присваивания переноса передает право собственности на управляемый ресурс в текущий объект, освобождая старый ресурс. Идиома move-and-swap упрощает реализацию.
Переход от lvalues
Иногда мы хотим перейти от lvalues. То есть, иногда мы хотим, чтобы компилятор обрабатывал lvalue, как если бы он был rvalue, поэтому он может вызывать конструктор перемещения, даже если он может быть потенциально опасным. Для этой цели C++ 11 предлагает стандартный шаблон функции библиотеки, называемый std::move
внутри заголовка <utility>
. Это имя немного неудачно, потому что std::move
просто std::move
lvalue в rvalue; он ничего не движет сам собой. Он просто позволяет двигаться. Возможно, это должно было быть названо std::cast_to_rvalue
или std::enable_move
, но мы застряли с этим именем.
Вот как вы явно переходите из lvalue:
unique_ptr<Shape> a(new Triangle);
unique_ptr<Shape> b(a); // still an error
unique_ptr<Shape> c(std::move(a)); // okay
Обратите внимание, что после третьей линии, больше не владеет треугольником. a
Это хорошо, потому что явно писать std::move(a)
, мы сделали наши намерения ясно: "Дорогой конструктор, делать все, что вы хотите с для инициализации a
c
, я не забочусь о. Больше Вы можете иметь a
ваш путь с ". a
std::move(some_lvalue)
значение lvalue в rvalue, что дает возможность последующего перемещения.
Xvalues
Обратите внимание, что хотя std::move(a)
является rvalue, его оценка не создает временного объекта. Эта головоломка заставила комитет ввести третью категорию ценностей. Что-то, что может быть связано с ссылкой rvalue, даже если оно не является rvalue в традиционном смысле, называется значением xvalue (значение eXpiring). Традиционные значения были переименованы в prvalues (Pure rvalues).
Оба значения и значения x являются значениями r. Xvalues и lvalues являются как glvalues (Обобщенные lvalues). Отношения легче понять с помощью диаграммы:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Обратите внимание, что только значения x действительно новы; остальное просто связано с переименованием и группировкой.
C++ 98 rvalues известны как prvalues в C++ 11. Ментально замените все вхождения "rvalue" в предыдущих абзацах на "prvalue".
Перемещение функций
До сих пор мы видели движение в локальные переменные и в функциональные параметры. Но перемещение также возможно в противоположном направлении. Если функция возвращается по значению, некоторый объект на сайте вызова (возможно, локальная переменная или временный, но может быть любым объектом) инициализируется выражением после оператора return
в качестве аргумента для конструктора перемещения:
unique_ptr<Shape> make_triangle()
{
return unique_ptr<Shape>(new Triangle);
} \-----------------------------/
|
| temporary is moved into c
|
v
unique_ptr<Shape> c(make_triangle());
Возможно, удивительно, что автоматические объекты (локальные переменные, которые не объявлены как static
) также могут быть неявно перемещены из функций:
unique_ptr<Shape> make_square()
{
unique_ptr<Shape> result(new Square);
return result; // note the missing std::move
}
Почему конструктор move принимает result
lvalue в качестве аргумента? Объем result
близок к завершению, и он будет уничтожен во время разматывания стека. После этого никто не мог жаловаться, что result
каким-то образом изменился; когда поток управления возвращается к вызывающему абоненту, result
больше не существует! По этой причине C++ 11 имеет специальное правило, позволяющее возвращать автоматические объекты из функций без необходимости записи std::move
. Фактически, вы никогда не должны использовать std::move
для перемещения автоматических объектов из функций, так как это блокирует "именованную оптимизацию возвращаемого значения" (NRVO).
Никогда не используйте std::move
для перемещения автоматических объектов из функций.
Обратите внимание, что в обеих заводских функциях тип возвращаемого значения является значением, а не ссылкой rvalue. Ссылки Rvalue по-прежнему являются ссылками, и, как всегда, вы никогда не должны возвращать ссылку на автоматический объект; вызывающий будет в конечном итоге с обвисшей ссылкой, если вы обманули компилятор в принятии вашего кода, например:
unique_ptr<Shape>&& flawed_attempt() // DO NOT DO THIS!
{
unique_ptr<Shape> very_bad_idea(new Square);
return std::move(very_bad_idea); // WRONG!
}
Никогда не возвращайте автоматические объекты по ссылке rvalue. Перемещение выполняется исключительно конструктором std::move
, а не std::move
, а не просто привязыванием rvalue к ссылке rvalue.
Перемещение в члены
Рано или поздно вы собираетесь написать такой код:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
В принципе, компилятор будет жаловаться, что 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)) // note the std::move
{}
};
Вы можете утверждать, что этот parameter
больше не используется после инициализации member
. Почему нет специального правила для молчания вставить std::move
же, как с возвращаемыми значениями? Наверное, потому что это слишком тяжело для разработчиков компилятора. Например, что, если тело конструктора находилось в другой единицы перевода? Напротив, правило возвращаемого значения просто должно проверять таблицы символов, чтобы определить, обозначает ли идентификатор после ключевого слова return
автоматический объект.
Вы также можете передать parameter
по значению. Для типов только для перемещения, таких как unique_ptr
, кажется, что еще нет установленной идиомы. Лично я предпочитаю передавать по значению, так как он вызывает меньше помех в интерфейсе.
Специальные функции участника
C++ 98 неявно объявляет три специальные функции-члены по требованию, то есть когда они где-то нужны: конструктор копирования, оператор назначения копирования и деструктор.
X::X(const X&); // copy constructor
X& X::operator=(const X&); // copy assignment operator
X::~X(); // destructor
Ссылки Rvalue прошли через несколько версий. Начиная с версии 3.0, C++ 11 объявляет две дополнительные специальные функции-члены по требованию: конструктор перемещения и оператор назначения перемещения. Обратите внимание, что ни VC10, ни VC11 не соответствуют версии 3.0, поэтому вам придется реализовать их самостоятельно.
X::X(X&&); // move constructor
X& X::operator=(X&&); // move assignment operator
Эти две новые специальные функции-члены объявляются неявно, если ни одна из специальных функций-членов не объявлена вручную. Кроме того, если вы объявите свой собственный конструктор перемещения или переместите оператор присваивания, ни конструктор копирования, ни оператор назначения копирования не будут объявлены неявно.
Что означают эти правила на практике?
Если вы пишете класс без неуправляемых ресурсов, нет необходимости объявлять какую-либо из пяти специальных функций-членов самостоятельно, и вы получите правильную семантику копирования и переместите семантику бесплатно. В противном случае вам придется реализовать специальные функции-члены самостоятельно. Конечно, если ваш класс не использует семантику перемещения, нет необходимости выполнять специальные операции перемещения.
Обратите внимание, что оператор присваивания копирования и оператор назначения перемещения могут быть объединены в единый унифицированный оператор присваивания, принимая его аргумент по значению:
X& X::operator=(X source) // unified assignment operator
{
swap(source); // see my first answer for an explanation
return *this;
}
Таким образом, количество специальных функций-членов для реализации падает с пяти до четырех. Здесь есть компромисс между безопасностью и эффективностью, но я не эксперт по этому вопросу.
Пересылка ссылок (ранее называемых универсальными ссылками)
Рассмотрим следующий шаблон функции:
template<typename T>
void foo(T&&);
Вы можете ожидать, что T&&
будет привязываться только к значениям rvalues, потому что на первый взгляд он выглядит как ссылка rvalue. Как оказалось, T&&
также связывается с lvalues:
foo(make_triangle()); // T is unique_ptr<Shape>, T&& is unique_ptr<Shape>&&
unique_ptr<Shape> a(new Triangle);
foo(a); // T is unique_ptr<Shape>&, T&& is unique_ptr<Shape>&
Если аргументом является r-значение типа X
, T
выводится как X
, поэтому T&&
означает X&&
. Это то, чего ожидали люди. Но если аргумент является lvalue типа X
, из-за специального правила T
выводится как X&
, поэтому T&&
будет означать что-то вроде X& &&
. Но поскольку C++ до сих пор не имеет понятия ссылок на ссылки, тип X& &&
сворачивается в X&
. Это может показаться запутанным и бесполезным вначале, но обращение к коллапсу важно для идеальной пересылки (что здесь не обсуждается).
T && не является ссылкой на rvalue, а ссылкой на пересылку. Он также связывается с lvalues, и в этом случае T
и T&&
являются ссылками на lvalue.
Если вы хотите ограничить шаблон функции до rvalues, вы можете комбинировать SFINAE с типом типа:
#include <type_traits>
template<typename T>
typename std::enable_if<std::is_rvalue_reference<T&&>::value, void>::type
foo(T&&);
Осуществление движения
Теперь, когда вы понимаете, что происходит свертывание ссылок, вот как реализуется 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
необходимо, так как в противном случае для lvalues типа X
возвращаемый тип был бы X& &&
, который бы рухнул на X&
. Так как t
всегда является lvalue (помните, что именованная команда rvalue является lvalue), но мы хотим привязать t
к ссылке rvalue, мы должны явно ввести t
в правильный тип возврата. Вызов функции, возвращающей ссылку rvalue, сам по себе является значением xvalue. Теперь вы знаете, где xvalues приходят из;)
Вызов функции, возвращающей ссылку rvalue, например std::move
, является значением xvalue.
Обратите внимание, что возврат в результате ссылки на rvalue в этом примере прекрасен, поскольку t
не обозначает автоматический объект, а вместо этого объект, который был передан вызывающим.
Ответ 3
Семантика переноса основана на ссылках rvalue.
Rvalue - временный объект, который будет уничтожен в конце выражения. В текущем С++ значения r привязаны только к const
ссылкам. С++ 1x позволит использовать ссылки const
rvalue, записанные T&&
, которые являются ссылками на объекты rvalue.
Так как rvalue будет умирать в конце выражения, вы можете украсть его данные. Вместо того, чтобы копировать его в другой объект, вы перемещаете его данные в него.
class X {
public:
X(X&& rhs) // ctor taking an rvalue reference, so-called move-ctor
: data_()
{
// since 'x' is an rvalue object, we can steal its data
this->swap(std::move(rhs));
// this will leave rhs with the empty data
}
void swap(X&& rhs);
// ...
};
// ...
X f();
X x = f(); // f() returns result as rvalue, so this calls move-ctor
В приведенном выше коде со старыми компиляторами результат f()
скопирован в x
с помощью конструктора x
copy. Если ваш компилятор поддерживает семантику перемещения, а x
имеет конструктор move, то вместо этого вызывается. Поскольку аргумент rhs
является rvalue, мы знаем, что он больше не нужен, и мы можем украсть его значение.
Таким образом, значение перемещено из неназванного временного объекта, возвращенного с f()
в x
(в то время как данные x
, инициализированные пустым x
, перемещаются во временное, что будет уничтожить после назначения).
Ответ 4
Предположим, что у вас есть функция, которая возвращает существенный объект:
Matrix multiply(const Matrix &a, const Matrix &b);
Когда вы пишете такой код:
Matrix r = multiply(a, b);
тогда обычный С++-компилятор создаст временный объект для результата multiply()
, вызовет конструктор копирования для инициализации r
, а затем уничтожит временное возвращаемое значение. Перемещение семантики в С++ 0x позволяет вызвать "move constructor" для инициализации r
путем копирования его содержимого, а затем отменить временное значение без его разрушения.
Это особенно важно, если (например, пример Matrix
выше), копируемый объект выделяет дополнительную память в куче для хранения своего внутреннего представления. Конструктор копирования должен либо сделать полную копию внутреннего представления, либо использовать семантику ссылок и семантику копирования на запись. Конструктор перемещения оставил только кучную память и просто скопировал указатель внутри объекта Matrix
.
Ответ 5
Если вы действительно заинтересованы в хорошем, углубленном объяснении семантики перемещения, я настоятельно рекомендую прочитать оригинальную бумагу, Предложение для добавления поддержки семантики перемещения на язык С++.
Это очень доступно и легко читается, и это отличное решение для преимуществ, которые они предлагают. Существуют и другие более свежие и современные документы о семантике перемещения, доступные на веб-сайте WG21, но этот, пожалуй, самый простой, поскольку он приближается к вещам от представления верхнего уровня и не вникает в подробные детали языка.
Ответ 6
Семантика перемещения переносит ресурсы, а не копирует их, когда больше не требуется исходное значение.
В С++ 03 объекты часто копируются, только для их уничтожения или назначения перед тем, как какой-либо код снова использует значение. Например, когда вы возвращаетесь по значению из функции, если только RVO не запускается - возвращаемое вами значение копируется в фрейм стека вызывающего абонента, а затем выходит за пределы области действия и уничтожается. Это всего лишь один из многих примеров: см. Pass-by-value, когда исходный объект является временным, алгоритмы вроде sort
, которые просто переупорядочивают элементы, перераспределение в vector
, когда превышено его capacity()
и т.д.
Когда такие пары "копировать/уничтожать" дороги, это обычно потому, что объект владеет каким-то тяжеловесным ресурсом. Например, vector<string>
может владеть динамически выделенным блоком памяти, содержащим массив объектов string
, каждый со своей собственной динамической памятью. Копирование такого объекта является дорогостоящим: вам необходимо выделить новую память для каждого динамически выделенного блока в источнике и скопировать все значения в. Затем вам нужно освободить всю память, которую вы только что скопировали. Однако перемещение большого vector<string>
означает просто копирование нескольких указателей (которые относятся к блоку динамической памяти) к месту назначения и обнуление их в источнике.
Ответ 7
В простых (практических) терминах:
Копирование объекта означает копирование его "статических" элементов и вызов оператора new
для его динамических объектов. Правильно?
class A
{
int i, *p;
public:
A(const A& a) : i(a.i), p(new int(*a.p)) {}
~A() { delete p; }
};
Однако, чтобы переместить объект (повторяю, с практической точки зрения), нужно только скопировать указатели на динамические объекты, а не создавать новые.
Но разве это не опасно? Конечно, вы можете разрушить динамический объект дважды (ошибка сегментации). Таким образом, чтобы этого избежать, вы должны "недействить" указатели источника, чтобы избежать их разрушения дважды:
class A
{
int i, *p;
public:
// Movement of an object inside a copy constructor.
A(const A& a) : i(a.i), p(a.p)
{
a.p = nullptr; // pointer invalidated.
}
~A() { delete p; }
// Deleting NULL, 0 or nullptr (address 0x0) is safe.
};
Хорошо, но если я перемещаю объект, исходный объект становится бесполезным, нет? Конечно, но в некоторых ситуациях это очень полезно. Наиболее очевидным является то, что когда я вызываю функцию с анонимным объектом (временным, rvalue объектом,..., вы можете называть его разными именами):
void heavyFunction(HeavyType());
В этой ситуации создается анонимный объект, затем копируется в параметр функции, а затем удаляется. Итак, здесь лучше перемещать объект, потому что вам не нужен анонимный объект, и вы можете сэкономить время и память.
Это приводит к понятию ссылки "rvalue". Они существуют в С++ 11 только для того, чтобы определить, является ли полученный объект анонимным или нет. Я думаю, вы уже знаете, что "lvalue" - это назначаемый объект (левая часть оператора =
), поэтому вам нужна именованная ссылка на объект, способный действовать как lvalue. Rvalue - это точно противоположное, объект без названных ссылок. Из-за этого анонимный объект и rvalue являются синонимами. Итак:
class A
{
int i, *p;
public:
// Copy
A(const A& a) : i(a.i), p(new int(*a.p)) {}
// Movement (&& means "rvalue reference to")
A(A&& a) : i(a.i), p(a.p)
{
a.p = nullptr;
}
~A() { delete p; }
};
В этом случае, когда объект типа A
должен быть "скопирован", компилятор создает ссылку lvalue или ссылку на rvalue в соответствии с именем или именем переданного объекта. Когда нет, ваш конструктор move вызывается, и вы знаете, что объект является временным, и вы можете перемещать его динамические объекты, а не копировать их, экономя пространство и память.
Важно помнить, что "статические" объекты всегда копируются. Нет способов "переместить" статический объект (объект в стеке, а не в кучу). Таким образом, различие "move" / "copy", когда объект не имеет динамических членов (прямо или косвенно), не имеет значения.
Если ваш объект является сложным, а деструктор имеет другие вторичные эффекты, такие как вызов функции библиотеки, вызов других глобальных функций или что-то еще, возможно, лучше сигнализировать движение с флагом:
class Heavy
{
bool b_moved;
// staff
public:
A(const A& a) { /* definition */ }
A(A&& a) : // initialization list
{
a.b_moved = true;
}
~A() { if (!b_moved) /* destruct object */ }
};
Итак, ваш код короче (вам не нужно назначать nullptr
для каждого динамического элемента) и более общий.
Другой типичный вопрос: в чем разница между A&&
и const A&&
? Конечно, в первом случае вы можете изменить объект, а во втором - нет, но практический смысл? Во втором случае вы не можете изменить его, поэтому у вас нет способов сделать недействительным объект (кроме как с изменяемым флагом или что-то в этом роде), и нет никакой практической разницы с конструктором копирования.
А что такое совершенная переадресация? Важно знать, что "ссылка на rvalue" является ссылкой на именованный объект в "области вызова". Но в фактическом объеме ссылка rvalue является именем объекта, поэтому он действует как именованный объект. Если вы передаете ссылку rvalue на другую функцию, вы передаете именованный объект, поэтому объект не принимается как временный объект.
void some_function(A&& a)
{
other_function(a);
}
Объект A
будет скопирован в фактический параметр other_function
. Если вы хотите, чтобы объект A
продолжал обрабатываться как временный объект, вы должны использовать функцию std::move
:
other_function(std::move(a));
С помощью этой строки std::move
будет отличать A
от rvalue, а other_function
получит объект как неназванный объект. Конечно, если other_function
не имеет специфической перегрузки для работы с неназванными объектами, это различие не имеет значения.
Это идеальная пересылка? Нет, но мы очень близки. Идеальная пересылка полезна только для работы с шаблонами, с целью сказать: если мне нужно передать объект другой функции, мне нужно, чтобы, если я получаю именованный объект, объект передается как именованный объект, а когда нет, Я хочу передать его как неназванный объект:
template<typename T>
void some_function(T&& a)
{
other_function(std::forward<T>(a));
}
Это сигнатура прототипической функции, которая использует совершенную пересылку, реализованную в С++ 11 с помощью std::forward
. Эта функция использует некоторые правила создания экземпляра шаблона:
`A& && == A&`
`A&& && == A&&`
Итак, если T
является ссылкой lvalue на A
( T= A &), A
также ( A && & = > A &). Если T
является ссылкой rvalue на A
, A
также (A && & = > A & &). В обоих случаях A
является именованным объектом в действительной области, но T
содержит информацию о его "ссылочном типе" с точки зрения области вызова. Эта информация (T
) передается как параметр шаблона в forward
, а 'a' перемещается или нет в соответствии с типом T
.
Ответ 8
Это похоже на семантику копирования, но вместо того, чтобы дублировать все данные, которые вы получаете, чтобы украсть данные из объекта, "перемещенного" из.
Ответ 9
Вы знаете, что означает семантика копии? это означает, что у вас есть типы, которые можно копировать, для определяемых пользователем типов, которые вы определяете это, либо купите явно написанную конструктор копирования, либо оператор присваивания, или компилятор генерирует их неявно. Это сделает копию.
Перемещение семантики - это, в основном, определяемый пользователем тип с конструктором, который принимает ссылку на r-значение (новый тип ссылки с использованием && (yes two ampersands)), который не является константой, это называется конструктором перемещения, то же относится к оператору присваивания. Итак, что делает конструктор перемещения, а вместо того, чтобы копировать память из его исходного аргумента, он "перемещает" память из источника в пункт назначения.
Когда вы захотите это сделать? хорошо std::vector является примером, скажем, вы создали временный std::vector и вы возвращаете его из функции say:
std::vector<foo> get_foos();
У вас возникнут накладные расходы из конструктора копирования, когда функция вернется, если (и он будет в С++ 0x) std::vector имеет конструктор перемещения вместо копирования, он может просто установить его указатели и "переместить", динамически распределенной памяти для нового экземпляра. Это похоже на передачу семантики передачи с std:: auto_ptr.
Ответ 10
Чтобы проиллюстрировать необходимость семантики перемещения, рассмотрим этот пример без семантики перемещения:
Здесь функция, которая принимает объект типа T
и возвращает объект того же типа T
:
T f(T o) { return o; }
//^^^ new object constructed
Вышеупомянутая функция использует вызов по значению, что означает, что когда эта функция вызывается, объект должен быть сконструирован для использования функцией.
Поскольку функция также возвращается по значению, для возвращаемого значения создается другой новый объект:
T b = f(a);
//^ new object constructed
Были построены два новых объекта, один из которых является временным объектом, который используется только для продолжительности функции.
Когда новый объект создается из возвращаемого значения, вызывается конструктор копирования для копирования содержимого временного объекта в новый объект b. После завершения функции временный объект, используемый в этой функции, выходит из области действия и уничтожается.
Теперь рассмотрим, что делает конструктор копирования.
Он должен сначала инициализировать объект, а затем скопировать все соответствующие данные из старого объекта в новый.
В зависимости от класса, возможно, это контейнер с очень большим количеством данных, то это может означать много времени и использования памяти
// Copy constructor
T::T(T &old) {
copy_data(m_a, old.m_a);
copy_data(m_b, old.m_b);
copy_data(m_c, old.m_c);
}
С семантикой перемещения теперь можно сделать большую часть этой работы менее неприятной, просто перемещая данные, а не копируя.
// Move constructor
T::T(T &&old) noexcept {
m_a = std::move(old.m_a);
m_b = std::move(old.m_b);
m_c = std::move(old.m_c);
}
Перемещение данных связано с повторным связыванием данных с новым объектом. И никакой копии не происходит вообще.
Это выполняется с помощью ссылки rvalue
.
Ссылка rvalue
работает очень похоже на ссылку lvalue
с одним важным отличием:
ссылку rvalue можно перемещать, а значение lvalue не может.
От cppreference.com:
Чтобы сделать надежную исключающую гарантию возможной, пользовательские конструкторы перемещения не должны генерировать исключения. На самом деле стандартные контейнеры обычно полагаются на std :: move_if_noexcept, чтобы выбирать между перемещением и копированием, когда элементы контейнера необходимо переместить. Если предусмотрены как конструкторы копирования, так и перемещения, разрешение перегрузки выбирает конструктор перемещения, если аргумент представляет собой rvalue (либо значение praleue, такое как безымянное временное, либо значение x, такое как результат std :: move), и выбирает конструктор копирования, если аргумент - это lvalue (именованный объект или оператор/оператор, возвращающий ссылку lvalue). Если предоставлен только конструктор копирования, все его категории выбирают (до тех пор, пока он принимает ссылку на const, поскольку rvalues может связывать ссылки const), что делает копирование резервной копии для перемещения, когда перемещение недоступно. Во многих ситуациях перемещение конструкторов оптимизируется, даже если они будут производить наблюдаемые побочные эффекты, см. Копирование elision. Конструктор называется "конструктором перемещения", когда он принимает значение rvalue в качестве параметра. Он не обязан ничего перемещать, класс не требует, чтобы ресурс был перемещен, а "конструктор перемещения" не мог перемещать ресурс, как в допустимом (но, возможно, не разумном) случае, когда параметр является const rvalue reference (const T &&).
Ответ 11
Я пишу это, чтобы убедиться, что я правильно понимаю.
Перенос семантики был создан, чтобы избежать ненужного копирования больших объектов. Бьярне Страуструп в своей книге "Язык программирования на C++" использует два примера, когда по умолчанию происходит ненужное копирование: одно, обкатка двух больших объектов и два - возврат большого объекта из метода.
Обмен двумя большими объектами обычно связан с копированием первого объекта во временный объект, копированием второго объекта в первый объект и копированием временного объекта во второй объект. Для встроенного типа это очень быстро, но для больших объектов эти три копии могут занимать много времени. "Назначение перемещения" позволяет программисту переопределить поведение копии по умолчанию и вместо этого заменять ссылки на объекты, а это значит, что копирования вообще нет, а операция свопинга выполняется намного быстрее. Назначение перемещения может быть вызвано вызовом метода std:: move().
Возврат объекта из метода по умолчанию включает в себя создание копии локального объекта и связанных с ним данных в местоположении, доступном для вызывающего (поскольку локальный объект недоступен для вызывающего и исчезает при завершении метода), Когда возвращается встроенный тип, эта операция выполняется очень быстро, но если возвращается большой объект, это может занять много времени. Конструктор перемещения позволяет программисту переопределить это поведение по умолчанию и вместо этого "повторно использовать" данные кучи, связанные с локальным объектом, указав объект, который возвращается вызывающему, чтобы кучи данных, связанных с локальным объектом. Таким образом, копирование не требуется.
В языках, которые не позволяют создавать локальные объекты (то есть объекты в стеке), эти типы проблем не возникают, поскольку все объекты выделены в куче и к ним всегда обращаются по ссылке.