Моим первым ответом было предельно упрощенное введение в перемещение семантики, и многие детали были упущены с целью упростить его. Тем не менее, есть еще много чего изменить семантику, и я подумал, что пришло время для второго ответа, чтобы заполнить пробелы. Первый ответ уже довольно старый, и было бы неправильно просто заменить его совершенно другим текстом. Я думаю, что это все еще служит хорошим введением. Но если вы хотите копать глубже, читайте дальше :)
Стефан Т. Лававей нашел время, чтобы дать ценные отзывы. Большое спасибо, Стефан!
Введение
Семантика перемещения позволяет объекту при определенных условиях вступать во владение внешними ресурсами какого-либо другого объекта. Это важно двумя способами:
Превращение дорогих копий в дешевые ходы. Смотрите мой первый ответ для примера. Обратите внимание, что если объект не управляет хотя бы одним внешним ресурсом (напрямую или косвенно через свои объекты-члены), семантика перемещения не даст никаких преимуществ по сравнению с семантикой копирования. В этом случае копирование объекта и перемещение объекта означают одно и то же:
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_ptrC ++ 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что не обозначает автоматический объект, но вместо этого объект, который был передан вызывающей стороной.