Вместо КДПВ — короткая драма для привлечения внимания, основанная на реальных событиях. Ее можно смело пропустить и перейти к статье, которая поможет вам разобраться в rvalue-ссылках, конструкторах перемещения, универсальных ссылках, идеальной передаче (perfect forwarding) и т. д. Действие первое Компилятор. Локальный объект x типа T, проживающий на стеке, вы приговариваетесь к изъятию у вас всего имущества в связи с тем, что не будете пользоваться им до конца своей жизни. Объект x. Что? Я не какой-то там временный объект, у меня постоянная регистрация, вы не имеете права! Компилятор. Никто вас не выселяет. Но согласно одиннадцатой редакции стандартного кодекса, все ваши вещи будут переданы другому объекту, которому они нужны больше. Объект x. И как вы это сделаете? Все мои данные надежно инкапсулированы, я не позволю никому бесцеремонно обращаться с ними. Если уж они так вам нужны, то пусть приходит конструктор копирования со своей флешкой, я ему скопирую. Конструктор. Это долго и неэффективно. Объект x. Может быть, вы собираетесь разбить мне окно reinterpret_cast-ом, и шариться в темноте? Компилятор. Нет-нет, ваш случай слишком заурядный, чтобы пользоваться услугами коллекторов. Мы просто вызовем функцию std::move, она наденет на вас static_cast<T&&>, и все будут думать, что вы являетесь временным объектом, то есть rvalue-выражением. Объект x. Ну и что, static_cast никак меня не затронет. Как только я доберусь до первой точки следования, я сниму его. Компилятор. Не сомневаюсь, но раньше чем вы это сделаете, вы встретите конструктор перемещения, уже поджидающего за дверью. Кажется вы еще не успели с ним познакомиться... Действие второе T(T&&). О, привет, временный объект! Объект x. Не-не-не, я lvalue, просто на мне static_cast. T(T&&). Все так говорят. Ну-ка, что у тебя есть? Давай сюда... Действие третье (эпилог) T(const T&). Я что-то не в восторге от твоих насильственных методов. T(T&&). Ты преувеличиваешь, сам подумай, зачем умирающему объекту все это барахло? Если бы не было меня, ты бы долго делал копию, а сразу за тобой деструктор бы уничтожил оригинал. Глупо. T(const T&). Так может он не врал, и он не временный? T(T&&). Мне без разницы. Значит кто-то решил, что так надо. Все законно, я просто делаю свою работу. T(const T&). Но раньше же прекрасно справлялись без тебя. Как-то организовывали передачу и возврат, или выделяли объекту жилплощадь в динамической памяти. Да и компилятор помогал с оптимизацией. T(T&&). Да ну, вспомни всю эту бюрократию. Объект заселяли, потом запись о регистрации теряли и все, он там так и жил до конца, и не прогнать. Да хватит уже жалеть эти объекты, а то ты превратишься в моего братца T(const T&&) — тот еще более сердобольный. Я ему говорю: "Все, объект уже не жилец, забирай его пожитки", а он все мнется, мол неудобно, давай я просто скопирую. T(const T&). У меня тоже есть есть брат T(T&), самый настоящий бандит. Маскируется под меня, под конструктор копирования… Дальше придумывать лень. Новые концепции не всегда ровно укладываются в голове. У меня так произошло с rvalue-ссылками. Вроде все очень просто, ясны предпосылки их появления, но при попытке чтения кода, насыщенного различными &&, обернутыми в кучу шаблонов, понимаешь, что ничего, в общем-то, и не понимаешь. Моя ошибка при изучении этой темы была в том, что я представлял rvalue-ссылки как принципиально новую сущность. Возможно это покажется вам странным, ведь во всех руководствах явно говорят, что это просто ссылка на rvalue. Это я понимал. Но ведь вместе с ними появилась куча новых понятий, типа универсальных ссылок и perfect forwarding. А еще, вызов функции, возвращающей &&, стал каким-то мистическим xvalue-выражением. Короче, было бы слишком просто считать их обычными ссылками. Так вот, самое главное — не усложняйте! Если вы увидели T&& ref = foo(), и не знаете как теперь относиться к ref, то относитесь к нему как к старой-доброй константной ссылке на rvalue: const T& ref = foo(), только без const. А почему нельзя было просто разрешить брать ссылку на rvalue? Иначе мы бы сразу теряли информацию о том, было ли выражение lvalue или rvalue. Сейчас же rvalue "предпочитает" быть переданным в функцию c аргументом T&&, а lvalue — с T&. Это дает нам возможность поступать с объектами по-разному, что особенно важно для реализации конструкторов копирования и перемещения. Еще одна моя ошибка — проверка примеров в Visual Studio. Примеры в статьях, например std::string &str = std::string("42"), которые не должны компилироваться, у меня компилировались. Это происходило из-за нестандартного расширения языка от Visual Studio. Я еще буду говорить об этом, потому понимание такого поведения очень важно, если VS — ваша среда разработки. Лучший способ чтения этой статьи — не верить мне, и все проверять самостоятельно. Советую в отладочной сборке. Если вы используете GCC, то будет неплохо ключом -fno-elide-constructors отключить технику Copy Elision, подавляющую вызов конструкторов копирования и перемещения там где можно. А если VS, то включите 4-й уровень предупреждений, для отлова использований нестандартного расширения. При изучении rvalue-ссылок и конструкторов перемещения, часто можно встретить подобные примеры: Временный объект копируется и сразу уничтожается. Это, конечно, явно избыточная операция, и работа конструктора перемещения здесь довольно наглядна. Однако, добавив конструктор перемещения в свой класс, вы можете не заметить ускорения. Ведь компилятор использует различные техники оптимизации, в частности Return Value Optimization, о которой мы немного поговорим в конце статьи. Я предлагаю следующий пример. Представим, что мы заполнили большой локальный вектор: В памяти он выглядит так (не будем переусложнять схему третьим указателем на конец зарезервированной памяти):  И хотим передать его некоторому объекту, через setter: Как было раньше: вектор передавался по константной ссылке (что позволяло использовать и lvalue и rvalue), а затем вызывался конструктор копирования, который создавал такой же большой вектор. А оригинал удалялся только после выхода из области видимости. Хотя хотелось бы просто передать новому вектору указатель на данные:  Сейчас это легко: А в методе setItems: Обратите внимание, что вектор передается по значению, что обеспечивает копирование для lvalue и перемещение для rvalue (в частности временных объектов). Таким образом, если локальный вектор вам еще нужен, то просто вызывайте setItems без std::move. Небольшой накладной расход в том, что аргумент еще раз перемещается с помощью оператора перемещения. Все что нам нужно сделать, чтобы разобраться со всеми концепциями — сфокусировать внимание на различных способах инициализации. Это может быть инициализация переменной: Передача аргумента: Возврат значения: Все эти случаи будем называть инициализацией, так как они семантически идентичны. Затем рассмотрим тип T. Он может быть одим из следующих: Уточню, что тип с указателем тоже относится к одному из них. Например, A* — это бессылочный тип, A*& — lvalue-ссылка, и т.д. Далее обратим внимание на выражение expr. Выражение имеет тип и категорию значения (rvalue или lvalue). Тип выражения для нас будет не так важен, как можно было бы подумать. В данной теме основную роль играет категория значения выражения: rvalue или lvalue, которую для краткости будем называть категорией выражения. Таким образом, у нас слева 3 варианта, справа 2. Итого 6. Прежде чем рассматривать их подробнее, научимся определять категорию. До c++11 существовали только эти 2 категории. С появлением move-семантики категория rvalue разбилась еще на 2 категории, о которых мы поговорим в следующем подразделе. Итак, категория каждого выражения является либо lvalue либо rvalue. Стандарт, конечно, описывает, что относится к каждому из них, но читать его сложно. Скотт Майерс предлагает следующие правила: Мне они не очень нравятся, так как они нестрогие, и существует много тонкостей, которые иногда меняются. И, самое главное, при изучении темы, как я могу понять, можно ли взять адрес? Ну, хорошо, мы знаем, что у временного объекта нельзя. А у str? Выясняется, что да, можно, так как str — lvalue, хотя его тип rvalue-ссылка. Запомните, это важно. Если нужно, я обращаюсь к cppreference: value category, где приведен перечень выражений. Некоторые примеры оттуда: Как уже упоминалось, rvalue разбилась на две категории xvalue (eXpiring lvalue) и prvalue (pure rvalue). А lvalue вместе с xvalue стала называться glvalue (generalized lvalue). Теперь, для наибольшей точности, следует относить выражения к одной из 3 категорий. На диаграмме это выглядит так:  К xvalue относятся следующие выражения: Для чего были нужны дополнительные категории? Выражение xvalue, хотя и является rvalue, но имеет некоторые свойства lvalue, например, оно может быть полиморфным. Далее нам не понадобятся эти дополнительные категории, но они бывают полезны для повышения своего авторитета во время жарких дискуссий. По нашей оценке вышло 6 вариантов. На самом деле нужно рассмотреть всего 3. Потому что, во-первых, нельзя инициализировать rvalue-ссылку lvalue-выражением. А во-вторых, инициалиция объекта (нессылочный тип) некоторым выражением, производится с помощью конструктора копирования или перемещения через передачу выражения (в конструктор) по ссылке, что сводит эти случаи к одному из трех вариантов. В качестве первоначального ориентира такая схема:  Обычная ссылка. Это работает только в двух случаях. До c++11 это был единственный способ передать куда-то временный объект. Например, в конструктор копирования: или куда-нибудь еще: Как вы знаете, константная ссылка продлевает жизнь временного объекта. Да, но только пока эта ссылка еще находится в области видимости. Так как временный объект располагается на стеке, то при выходе из области видимости или из функции, компилятор уже не обязан отвечать за судьбу объекта. Пусть вас не удивляет прекрасно работающий в Visual Studio пример: Ответ появится только на 4 уровне предупреждений: Это нестандартное для C++ расширение отключается ключом /Za (Disable Language Extensions), но некоторые хедеры, типа Windows.h, не будут компилироваться, так как в расширение входят и другие фичи. Тут все просто: так писать нельзя. Будьте внимательны, на Хабре есть перевод статьи «A Brief Introduction to Rvalue References» (статья аж 2008 года), который находится первым в выдаче поисковиков по запросу "rvalue". Пример оттуда ошибочен: Также, там приведена неверная реализация std::move. Впрочем, в комментариях указали на ошибки. Подобрались к самому интересному. Начнем с простейших примеров: Эти ссылки ведут себя как обычные. Можно представить ссылку const T&, которую можно менять, или вспомнить расширение Visual Studio. Может возникнуть вопрос, почему же нельзя было просто использовать обычную ссылку для всех категорий выражений? В VS это же прекрасно работало (для классов). Ссылки && дают возможность перегружать функции и особенно конструкторы не только по типу выражения, но и по категории (lvalue или rvalue). Пример с 2 конструкторами: Выражение s1 + s2 является rvalue, и для него подходят оба конструктора (см. схему в начале раздела). Приоритет будет отдан типу string&&. Те, кто знаком с конструкторами перемещения, знает почему это важно. Перед тем как поговорить об этом подробнее, разберемся с приоритетами. В большинстве ситуаций достаточно знать, что T&& "приоритетнее" const T&. Но желательно разобраться и с const T&& и с T&. Вот расширенная схема:  Правила простые (в порядке убывания приоритета): Когда вы пишете: вызывается конструктор. Какой? У класса их может быть несколько: При вызове конструктора происходит передача выражения по ссылке согласно схеме выше, в зависимости от категории выражения и типа ссылки аргументов. Теперь о том, что делает каждый конструктор. Обычный конструктор копирования. Имеет самый низкий приоритет, зато может принимать любые выражения. Я называю его коварным конструктором копирования, потому что он может совершенно спокойно модифицировать источник, если изначально он не был константным. Имеет приоритет выше обычного конструктора копирования. Сомневаюсь, что он используется на практике. Если есть хорошие примеры его применения, то напишите. Этот может принимать rvalue-выражения, т. е. объекты срок жизни которых подошел к завершению. Умирающий объект говорит: я умираю, возьми мои гранаты указатели на данные, мне они уже не нужны. А конструктор отвечает: нет, я так не могу, они же твои и останутся с тобой, я могу только сделать копию. Мне также неизвестен практический пример использования. Этот конструктор вызывается только для неконстантного rvalue. Такое выражение представляет собой временный объект или объект, приведенный к rvalue-ссылке с помощью static_cast<T&&>. Подобное преобразование никак не затрагивает объект, однако такое выражение-обертка может быть передано по rvalue-ссылке в конструктор перемещения (или в какую-нибудь функцию). Конструктор забирает все указатели на данные и другие члены класса и передает их новому объекту. Поэтому, для эффективной работы move-конструктора, членов класса должно быть меньше. В предельном случае можно лишь хранить один указатель на реализацию. Здесь хорошо подходит идиома Pimpl (pointer to implementation). Например, рассмотрим класс строки, заданной с помощью указателя на данные в куче и длины строки (это только концептуальный пример): Так может выглядеть конструктор перемещения: Что бы получилось, если бы мы не обнулили его данные (3-4 строки конструктора)? Рано или поздно для двух объектов-двойников был бы вызван деструктор, который бы попытался два раза удалить одни и те же данные. Но что если обнулить только указатель на данные, но не длину? Ведь деструктор отработает дважды нормально — конкретно этот не пользуется длиной. Тогда пользователь объекта, который вдруг воспользуется функцией getLength получит неверные сведения. Поэтому, нельзя варварски обращаться с объектом, который уже не нужен. В любом случае, необходимо оставлять его пустым, но в корректном состоянии. К тому же, можно вызывать перемещение несколько раз на протяжении его жизни. Мы уже говорили про static_cast<T&&> при обсуждении конструктора перемещения. Как вы помните, выражение "обернутое" в static_cast<T&&>, сигнализирует, что объект может быть перемещен. Тогда, если инициализируется новый объект, будет вызван конструктор перемещения, а не копирования. Теперь мы можем реализовать задачу из начала статьи: Вы наверняка знаете, что есть более удобный способ — вызов функции std::move, которая является оберткой над static_cast. Чтобы понять как она работает, мы будем писать ее самостоятельно и наступим на все грабли, зато это будет полезным уроком. Прежде чем читать дальше, вы можете написать ее сами. Конечно эта функция должна быть шаблонной, чтобы принимать разные типы. Но пока, для простоты, сделаем для одного конкретного класса A. Порассуждаем. Если мы хотим передать lvalue, то сделать это можно только с помощью аргумента типа A&. Далее, мы преобразуем его к A&&, и типом возврата тоже будет A&&. Вызов функции, которая возвращает &&, является rvalue-выражением (точнее xvalue), как мы и хотели. Но стандартная функция std::move также принимает и rvalue, поэтому она универсальна, чего нельзя сказать о нашей. Одно из решений простое — добавить еще одну функцию с аргументом A&&: Работает и для lvalue и для rvalue. Но, подождите, настоящая std::move только одна и имеет шаблонный тип аргумента T&&. Как так? Будем разбираться дальше. Итак, мы выяснили, что шаблонная std::move принимает lvalue для аргумента T&&. Значит, T&& — это не rvalue-ссылка. T&& каким-то образом превращается в T&. Но каким типом инстанцирован T? Чтобы понять что к чему, попробуем вызывать две перегруженные функции для двух категорий выражений (rvalue и lvalue): В самих функциях будем проверять каким типом инстанцирован Т. Тип ссылки можно проверить так: Выясняется, что подходят 3 варианта: На следующей схеме подписи на стрелках означают каким типом был инстанцирован T в том или ином случае.  Первые два варинта предсказуемые, мы на них и полагались. Третий использут правило reference collapse — сжатие ссылок, которое определяет поведение при появлении ссылки на ссылку. Такая конструкция сама по себе является запрещенной, но возникает в шаблонах. Поэтому было задано, что если T инстанцирован A&&, то T&& (A&& &&) = A&&, а остальных случаях (ссылки на ссылку) тип равен A&, то есть: Таким образом, аргумент типа T&&, где T — шаблонный тип, может принимать оба типа ссылок. Поэтому ссылку типа T&& называют универсальной ссылкой. Теперь понятно, что std::move может примать T&& (см. foo(T&&) на схеме выше). Дабавим шаблонности нашей my_move: Это ошибочная реализация, ведь T&& превращается в A& для lvalue, и, в этом случае, инстанс функции будет таким: Тип аргумента корректный, но для остальных местах нам было нужно оставить A&& вместо A&. Пора смотреть реализацию std::move: Думаю, понятно: remove_reference убирает все ссылки, а затем к получившемуся типу добавляется &&. Кстати, класс remove_reference устроен очень просто. Это три специализации шаблонного класса с параметрами T, T&, T&&. Казалось бы, разобрались со всеми проблемами, но нет. Мы научились передавать все выражения по шаблону T&&, но до сих пор не интересовались, как работать с ними дальше. Функция std::move не считается, потому что она не задумываясь приводит все к rvalue. Зачем нужно различать категории выражений? Представим, что мы пишем аналог make_shared. Напоминаю, что make_shared<T>(...) сам вызывает конструктор T с заданными аргументами. Сейчас не время разбираться с variadic templates, поэтому, для простоты предположим, что аргумент всего один. Также будем возвращать самый обычный указатель. Возьмем простой класс: Мы хотим делать так: Воспользовавшись сжатием ссылок, пишем реализацию: Ну, работает, конечно. Только всегда происходит копирование. Нашли ошибку? Все просто — arg всегда lvalue. Кажется, что мы потеряли информацию о категории исходного выражения. Не совсем — ее еще можно извлечь из Arg. Ведь для lvalue: Arg=A&, для rvalue: Arg=A. Получается, что нам нужна функция, восстанавливающая тип ссылки, то есть: Попробуем реализовать для типа A (без шаблонов): Пока это не работает, так как сигнатуры одинаковы. Вспомним, что для первого случая у нас есть A&, а для второго просто A. Значит, нужно одним движением получить из A& — A&, а из A — A&&. Просто используем сжатие ссылок, "нацепив" на исходные типы &&. Стандартная версия std::forward немного сложнее. Она перегружена для различия & и &&, что позволяет организовать дополнительные проверки. Если проверять вызовы конструкторов/деструкторов при возврате объекта, то окажется, что в разных компиляторах при разных настройках количество вызовов будет различаться. Это сбивает с толку при изучении move-семантики. Сначала рассмотрим как происходит возврат объекта без оптимизаций. Для этого в GCC можно задать ключ -fno-elide-constructors. Затем изучим техники оптимизации, которые применяет компилятор. В следующем примере возвращаемое значение никак не используется. Во фрейме стека функции foo создается объект. Так как выполнение функции подходит к завершению, то ее память совсем скоро будет использована для других нужд. Поэтому, чтобы сохранить объект, компилятор делает примерно так: Это очень условная запись, которая нужна лишь для того, чтобы понять принцип. temp — временный объект, размещенный во фрейме вызывающей функции. Важно, что result, скорее всего, будет интерпретироваться компилятором как rvalue, если он является локальным объектом foo. Поэтому temp может быть создан с помощью конструктора копирования или перемещения. На всякий случай напомню, что возвращать ссылку (неважно & или &&) на локальный объект нельзя. Если нам нужно использовать результат, то такая запись: Превращается в: Такая двойная работа не обязательно будет применяться в вашем компиляторе. Однако, для изучения бывает полезно представлять возврат объекта именно так. Как видите, даже с отключенными оптимизациями можно возвращать большой объект (в смысле его данных, а не sizeof, конечно), если для него существует конструктор перемещения. Каждый C++ программист задумывался: можно ли возвращать большой вектор? Иногда страх пересиливал и вместо std::vector<int> get(); мы писали void get(std::vector<int> &); С move-конструкторами стало полегче. Но и до них компиляторами применялась copy elision — техника избавления от лишних конструкторов, там где можно. В частности, для возвращаемых значений используется return value optimization (RVO). Вместо возврата локального объекта через конструктор копирования (или перемещения), компилятор поступает примерно как и мы, когда передавали объект по ссылке. То есть, возвращаемый объект сразу создается в вызывающей функции, а заполняется в вызываемой. Можно ли полагаться на RVO? Пример в Visual Studio 2015: Скажем так, полагаться только на оптимизацию не стоит. Даже если она хорошо отрабатывает в релизной версии, то в отладочной она обычно не применяется, что может затянуть процесс. Пишите конструкторы перемещения или создавайте динамические объекты и заворачивайте их в std::shared_ptr или std::unique_ptr.