Моим первым ответом было предельно упрощенное введение в перемещение семантики, и многие детали были упущены с целью упростить его.Тем не менее, есть еще много чего изменить семантику, и я подумал, что пришло время для второго ответа, чтобы заполнить пробелы.Первый ответ уже довольно старый, и было бы неправильно просто заменить его совершенно другим текстом.Я думаю, что это все еще служит хорошим введением.Но если вы хотите копать глубже, читайте дальше:)
Стефан Т. Лававей потратил время и предоставил ценные отзывы.Большое спасибо, Стефан!
Введение
Семантика перемещения позволяет объекту при определенных условиях вступать во владение внешними ресурсами какого-либо другого объекта.Это важно двумя способами:
Превращение дорогих копий в дешевые ходы.Смотрите мой первый ответ для примера.Обратите внимание, что если объект не управляет хотя бы одним внешним ресурсом (напрямую или косвенно через свои объекты-члены), семантика перемещения не даст никаких преимуществ по сравнению с семантикой копирования.В этом случае копирование объекта и перемещение объекта означают одно и то же:
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<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
, которое обозначает * 1087, должно быть существенное различие* переменная и выражение make_triangle()
, которое обозначает вызов функции, которая возвращает значение auto_ptr
, создавая таким образом свежий временный объект auto_ptr
каждый раз, когда он вызывается.a
является примером lvalue , тогда как make_triangle()
является примером rvalue .
Переход от lvalues, таких как a
, опасен, потому что позже мы можем попытаться вызвать функцию-член через a
, вызывая неопределенное поведение.С другой стороны, переход от значений r, таких как make_triangle()
, совершенно безопасен, потому что после того, как конструктор копирования выполнил свою работу, мы не можем снова использовать временное значение.Нет выражения, которое обозначает временное;если мы просто напишем make_triangle()
снова, мы получим другое временное.На самом деле, временный перенос уже удален на следующую строку:
auto_ptr<Shape> c(make_triangle());
^ the moved-from temporary dies right here
Обратите внимание, что буквы l
и r
имеют историческое происхождение слева и справа.сторона задания.Это больше не верно в C ++, потому что есть l-значения, которые не могут появляться в левой части присваивания (например, массивы или пользовательские типы без оператора присваивания), и есть r-значения, которые могут (все r-значения типов классов).с оператором присваивания).
Значение класса - это выражение, оценка которого создает временный объект.При нормальных обстоятельствах никакое другое выражение внутри той же области действия не обозначает тот же временный объект.
Rvalue-ссылки
Теперь мы понимаем, что переход от lvalue потенциально опасен, но переход от rvaluesбезвредны.Если бы в C ++ была языковая поддержка, чтобы отличать аргументы lvalue от аргументов rvalue, мы могли бы либо полностью запретить переход от lvalue, либо, по крайней мере, сделать переход от lvalue явный на сайте вызова, чтобы мы больше не могли двигаться случайно.1120 *
C ++ 11 отвечает на эту проблему: rvalue reference .Ссылка на rvalue - это новый тип ссылок, который привязывается только к rvalue, а синтаксис - X&&
.Старая добрая ссылка X&
теперь известна как ссылка на значение .(Обратите внимание, что 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&&
- это новый тип ссылок, который привязывается только к rvalues.
Неявные преобразования
Rvalue ссылки прошли через несколько версий.Начиная с версии 2.1, ссылка на значение 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
, и r
привязывается к этому временному объекту.Это один из случаев, когда различие между значениями (выражениями) и временными значениями (объектами) несколько размыто.
Перемещение конструкторов
Полезный пример функции с параметром 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
может быть привязан только к rvalue. Это именно то, что мы хотели; опасные действия никогда не должны быть скрытыми. Третья строка компилируется просто отлично, потому что make_triangle()
- это значение. Конструктор перемещения переведет владение из временного в 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
выходит из области видимости, автоматически освобождая старый ресурс.
Оператор назначения перемещения передает владение управляемым ресурсом текущему объекту, освобождая старый ресурс.
Идиома перемещения и обмена упрощает реализацию.
Перемещение от lvalues
Иногда мы хотим отойти от lvalues. То есть иногда мы хотим, чтобы компилятор обрабатывал lvalue, как если бы он был rvalue, чтобы он мог вызывать конструктор move, даже если он потенциально может быть небезопасным.
Для этой цели C ++ 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); // 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 в традиционном смысле, называется xvalue (значение eXpiring). Традиционные значения были переименованы в prvalues (Чистые значения).
И prvalues, и xvalues являются rvalues. Xvalues и lvalues оба glvalues (Обобщенные lvalue). Отношения легче понять с помощью диаграммы:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Обратите внимание, что только значения xval действительно новые; остальное только из-за переименования и группировки.
C ++ 98 Значения известны как 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
}
Почемуконструктор перемещения принимает lvalue result
в качестве аргумента?Область действия result
подходит к концу и будет уничтожена при разматывании стека.Впоследствии никто не мог жаловаться, что result
как-то изменился;когда поток управления возвращается к вызывающей стороне, result
больше не существует!По этой причине в C ++ 11 есть специальное правило, которое позволяет автоматически возвращать объекты из функций без необходимости писать std::move
.Фактически, вы должны никогда использовать std::move
для перемещения автоматических объектов из функций, поскольку это запрещает «оптимизацию именованного возвращаемого значения» (NRVO).
Никогда не используйте std::move
для перемещения автоматических объектов из функций.
Обратите внимание, что в обеих фабричных функциях тип возвращаемого значения - это значение, а не ссылка на значение.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
и не просто привязывает rvalue к ссылке на rvalue.
Переход к элементам
Рано или поздно высобираемся написать код, подобный этому:
class Foo
{
unique_ptr<Shape> member;
public:
Foo(unique_ptr<Shape>&& parameter)
: member(parameter) // error
{}
};
По сути, компилятор будет жаловаться, что parameter
является lvalue.Если вы посмотрите на его тип, вы увидите ссылку 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&&
будет привязываться только к rvalue, потому что на первый взгляд это похоже на ссылку на 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>&
Если аргумент является значением типа X
, T
выводится как X
, следовательно, T&&
означает X&&
. Это то, что можно было ожидать.
Но если аргумент является lvalue типа X
, то из-за специального правила T
выводится как X&
, следовательно, T&&
будет означать что-то вроде X& &&
. Но поскольку в C ++ до сих пор нет понятия ссылок на ссылки, тип X& &&
является , свернутым в X&
. Поначалу это может показаться запутанным и бесполезным, но свертывание ссылок необходимо для совершенной пересылки (что здесь не обсуждается).
T && - это не ссылка на значение, а ссылка на пересылку. Он также привязывается к lvalue, в этом случае T
и T&&
являются ссылками lvalue.
Если вы хотите ограничить шаблон функции значениями r, вы можете объединить 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
не обозначает автоматический объект, но вместо этого объект, который был передан вызывающей стороной.