Моим первым ответом было предельно упрощенное введение в перемещение семантики, и многие детали были упущены с целью упростить его. Тем не менее, есть еще много чего изменить семантику, и я подумал, что пришло время для второго ответа, чтобы заполнить пробелы. Первый ответ уже довольно старый, и было бы неправильно просто заменить его совершенно другим текстом. Я думаю, что это все еще служит хорошим введением. Но если вы хотите копать глубже, читайте дальше :)
Стефан Т. Лававей нашел время, чтобы дать ценные отзывы. Большое спасибо, Стефан!
Введение
Семантика перемещения позволяет объекту при определенных условиях вступать во владение внешними ресурсами какого-либо другого объекта. Это важно двумя способами:
Превращение дорогих копий в дешевые ходы. Смотрите мой первый ответ для примера. Обратите внимание, что если объект не управляет хотя бы одним внешним ресурсом (напрямую или косвенно через свои объекты-члены), семантика перемещения не даст никаких преимуществ по сравнению с семантикой копирования. В этом случае копирование объекта и перемещение объекта означают одно и то же:
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
что то, что синтаксически выглядит как копия, на самом деле является движением. Попытка вызова функции-члена в Move-from 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
значение by , должно быть какое-то глубокое различие , создавая тем самым свежий временный auto_ptr
объект каждый раз, когда он вызывается. a
является примером lvalue , тогда как make_triangle()
является примером rvalue .
Переход от значений l, таких как 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 ссылки
Теперь мы понимаем, что переход от lvalues потенциально опасен, но переход от rvalues безвреден. Если бы в C ++ была языковая поддержка, чтобы отличать аргументы lvalue от аргументов rvalue, мы могли бы либо полностью запретить переход от lvalue, либо, по крайней мере, сделать переход от lvalue явным на сайте вызова, чтобы мы больше не перемещались случайно.
Ответ C ++ 11 на эту проблему - rvalue ссылки . Ссылка на rvalue - это новый вид ссылок, который привязывается только к rvalue, а синтаксис - это 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&&
. Ограничение чтения из значений не очень полезно.
Ссылка на rvalue X&&
- это новый вид ссылок, который привязывается только к rvalue.
Неявные преобразования
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
и 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. Значения xvalue и lvalue являются glvalues (Обобщенные lvalues). Отношения легче понять с помощью диаграммы:
expressions
/ \
/ \
/ \
glvalues rvalues
/ \ / \
/ \ / \
/ \ / \
lvalues xvalues prvalues
Обратите внимание, что только значения xval действительно новые; остальное только за счет переименования и группировки.
Значения в C ++ 98 известны как значения в 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!
}
Никогда не возвращайте автоматические объекты по ссылке. Перемещение выполняется исключительно конструктором перемещения 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 && - это не ссылка на значение, а ссылка на пересылку. Он также связывается с lvalues, в каком случае 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&&
и возвращает ссылку на значение. 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
что не обозначает автоматический объект, но вместо этого объект, который был передан вызывающей стороной.