std forward что это
std::move vs. std::forward
Несмотря на то, что материалов на тему move-семантики и идеальной передачи в Интернете предостаточно, вопросов типа «что я должен здесь использовать: move или forward?» не становится меньше или мне просто «везет» на них. Поэтому и решено было написать эту статью. Предполагается, что читатель хотя бы немного знаком с rvalue-ссылками, move-семантикой и идеальной передачей.
Для чего нужен шаблон функции std::move?
Функция std::move выполняет приведение передаваемого lvalue-аргумента в rvalue-ссылку. Зачем это нужно? Вернемся во времена С++98 и рассмотрим классический пример:
В этом примере для встроенных типов проблем не возникает, но для таких типов как vector или string копирование может являться крайне дорогой операцией. Необходимо было добавлять специализации для шаблона swap, чтобы выполнять это действие более эффективно, и многих такой подход не устраивал.
Что же здесь нужно изменить? Нам не нужно выполнять копирование, а нужно «перемещать» объекты. Как это сделать? Нужно вызывать специальный конструктор или оператор присваивания, которые не будут копировать содержимое классов, а будут обмениваться им (по сути выполнять swap для всех членов).
Для этого в C++11 ввели rvalue-ссылки, которые обозначаются через && и позволяют ссылаться на временные объекты или определять объекты как «перемещаемые». Также появились конструктор перемещения (move constructor) и оператор перемещающего присваивания (move assignment), которые отличаются от копирующих «коллег» тем, что в качестве аргумента принимают неконстантную rvalue-ссылку:
Теперь операцию swap мы можем переписать с использованием функции std::move, которая возвращает rvalue-ссылку и тем самым сообщает компилятору, что параметр является перемещаемым:
Функция std::move не выполняет никаких перемещений. Как уже было сказано выше, она выполняет приведение типа к rvalue-ссылке. Давайте посмотрим на ее код:
То есть это просто обертка для static_cast, которая «убирает» ссылку у переданного аргумента с помощью remove_reference_t и, добавив &&, преобразует тип в rvalue-ссылку. Давайте глянем на то, как именно remove_reference_t избавляется от ссылок:
Для чего нужен std::forward?
Функция std::forward, как известно, применяется при идеальной передаче (perfect forwarding).
Идеальная передача позволяет создавать функции-обертки, передающие параметры без каких-либо изменений (lvalue передаются как lvalue, а rvalue – как rvalue) и тут std::move нам не подходит, так как она безусловно приводит свой результат к rvalue.
Поэтому, была разработана функция std::forward, которая выполняет примерно следующую работу:
То есть, если ссылка была передана как rvalue, то вызываем духов std::move, а иначе просто возвращаем то, что передали.
Если посмотреть исходники стандартной библиотеки у MS, то мы увидим следующую реализацию:
Может быть немного сложнее, но смысл тот же. И пусть Вас не пугает перегруженная версия forward(remove_reference_t && _Arg). В ней просто добавлена проверка на случай компиляции чудесатых конструкций вроде такой:
На стадии компиляции получим сразу ошибку: bad forward call. И я полагаю, это единственное, ради чего добавлена еще одна сигнатура, поскольку нет никакого смысла городить, например, такие конструкции:
Если у кого-то есть идеи, для чего еще может понадобиться forward(remove_reference_t && _Arg), дайте знать и заранее спасибо.
Возможно сейчас у читателя возникает в голове вопрос: а может на самом деле нет нужды вызывать forward и просто вызывать static_cast (_Arg)? Да можно, конечно, просто forward более безопасен в плане очепяток и путаниц. Кроме того, forward (Arg) в коде выглядит более осмысленным и понятным, нежели static_cast (Arg). Никто же не любит писать комментарии, поэтому лучше писать сразу хорошо читаемый код. То же самое касается и move, и тут придется писать более громоздкую конструкцию:
Пример
Я скопировал код move и forward, вставив в них вывод в cout, чтобы можно было увидеть, что же происходит. Далее мы берем и последовательно вызываем функцию foo, передавая сперва lvalue значение, а затем rvalue. Обратите внимание, что у функции foo аргумент является «универсальной ссылкой» в терминологии Скотта Мейерса, или передаваемой ссылкой (forwarding reference) в терминологии комитета стандартизации С++, то есть она:
Внутри функции foo, вызываем перегруженную функцию bar для константной lvalue-ссылки, для lvalue-ссылки и, наконец, для «универсальной» ссылки.
На выводе получаем:
Итак, разбираем lvalue. В функции foo тип T будет выведен как int&. Почему T выводится как lvalue-сылка, т.е. как int&? По правилам вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано lvalue значение, то T выводится как lvalue-ссылка. Согласно правилам сжатия ссылок (reference collapsing) аргумент p также будет выведен как int&, так как foo(int& && p) превратится в foo(int& p). Таким образом, мы получаем следующую версию foo:
bar(p); вызовет bar(int& v), и тут вопросов возникать не должно.
bar(_move(p)); сперва вызовет move, которая вернет rvalue-ссылку, а значит будет вызвана bar(int&& v).
bar(_forward (p)); сперва вызовет _forward(int& _Arg), которая должна вернуть lvalue-ссылку, и значит будет вызвана опять bar(int& v). Что при этом произойдет внутри _forward? Применив правила сжатия ссылок, мы получим следующую версию _forward:
Разберем теперь rvalue. В функции foo тип T будет выведен как int. Согласно все тех же правил вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано rvalue, то T выводится как бессылочный тип. Это необходимо для корректной работы функции forward, как мы это увидим далее. Итак, наша foo выглядит теперь следующим образом:
bar(p); как и ранее, вызовет bar(int& v), т.к. p является l-value, не смотря на то, что выглядит как r-value ссылка.
bar(_move(p)); как и ранее, сперва вызовет move, которая вернет rvalue-ссылку, а значит будет опять вызвана bar(int&& v).
Теперь посмотрим как же будет инстанцирована forward :
Давайте подставим вместо move и forward их содержимое, чтобы наглядно посмотреть на разницу:
Вывод: для перемещаемых объектов необходимо использовать std::move, а для идеальной передачи – std::forward.
Примеры использования move и forward
Кроме приведенного выше примера std::swap, использование std:move можно найти в различных алгоритмах, где нужно менять элементы местами (различные сортировки, или, например, в функции std::unique).
Если необходимо «передать» умный указатель std::unique_ptr, то сделать мы можем это только через std::move (либо через release() и сырой указатель, но это не по фень-шую).
В функции std::vector::push_back для rvalue можно обнаружить:
Таким образом, legacy-код, добавляющий новый элемент в вектор через rvalue волшебным образом начинает работать через перемещение, а не копирование.
Если ваша функция возвращает кортеж (или пару), то стоит обратить внимание на возможность перемещения некоторых или даже всех его элементов:
Обратите внимание на то, что не нужно использовать std::move при возврате из функции, возвращающий локальный объект:
Здесь нужно убрать std::move. Всю работу сделает copy/move elision – специальная оптимизация, которую выполняет компилятор, убирая лишние создания объектов.
Функция std::forward и вариативные шаблоны являются фундаментом, на котором строятся такие функции-обертки как std::make_unique, std::make_shared, std::make_pair, std::make_tuple и другие. Например, make_unique делает очень простую работу:
Семейство emplace методов также работает через forward, зачастую просто вызывая конструктор через placement-new.
Что еще можно почитать на эту тему:
Книга Скотта Мейерса «Эффективный и современный С++. 42 рекомендации по использованию C++11 и C++14»
Зачем std::forward иметь две сигнатуры и явно указывать тип?
В стандарте std::forward имеет две сигнатуры (для lvalue и rvalue) и требует от пользователя явно указанного типа:
В то время, когда тип можно вывести и реализовать всего лишь одну сигнатуру. В static_cast сработает сворачивание ссылок и результат будет эквивалентным стандартной функции.
Средний 2 комментария
Евгений Шатунов, пиши в ответ, отмечу правильным
Однако, эта функция позволяет вывести T& t в ситуации, когда этот тип не может быть выведен (согласно стандарту С++, 14.8.2.5), поэтому необходимо явно указывать параметры шаблона при вызове std::forward.
Приведи еще пример, когда тип нельзя вывести, плиз
Думаю, вот эта статья даст ответы на твои вопросы.
Если максимально точечно, то это раздел «Реализация идеальной передачи с использованием std::forward» и цитата в нем.
Еще один момент, который нужно отметить: использование std::remove_reference. На самом деле forward может быть реализован и без использования этой функции. Сжатие ссылок выполнит всю работу, таким образом, применение std::remove_reference для этого избыточно. Однако, эта функция позволяет вывести T& t в ситуации, когда этот тип не может быть выведен (согласно стандарту С++, 14.8.2.5), поэтому необходимо явно указывать параметры шаблона при вызове std::forward.
В объяснение моих мыслей, код
И вот r-value на этот самый type и используется для инстанцирования вызовов с переданным к-value значением, а версия с l-value ссылкой на этот type используется для инстанцирования с l-value значением.
Это не так. std::remove_reference применяется только к параметру std::forward и нужен для реализации одного конкретного случая. Тип результата у std::forward все так же выводится с помощью reference collapse rule.
Вы думаете что возвращаемый ей результат является r-value, но это не так, она возвращает l-value!
Происходит это из-за сворачивания ссылок. decltype(auto) выведет rvalue-ссылку, есди ему передать rvalue-ссылку. Передает ему rvalue ссылку static_cast, который кастует параметр к T&&.
Идеальная передача и универсальные ссылки в C++
Недавно на isocpp.org была опубликована ссылка на статью Eli Bendersky «Perfect forwarding and universal references in C++». В этой небольшой статье есть простой ответ на простой вопрос — для решения каких задач и как нужно использовать rvalue-ссылки.
Одно из нововведений C++11, которое нацелено на увеличение эффективности программ – это семейство методов emplace у контейнеров STL. Например, в std::vector появился метод emplace_back (практически аналог метода push_back) и метод emplace (практически аналог метода insert).
Вот небольшой пример, показывающий предназначение этих новых методов:
Если проследить за вызовами конструкторов и деструкторов класса MyKlass, во время вызова push_back можно увидеть следующее:
Проблема идеальной передачи
Допустим, есть некоторая функция func, принимающая параметры типов E1, E2, …, En. Требуется создать функцию wrapper, принимающую такой же набор параметров. Другими словами – определить функцию, которая передаст принимаемые параметры в другую функцию, не создавая временные переменные, то есть выполнит идеальную передачу.
Для того чтобы конкретизировать задачу, рассмотрим метод emplace_back, который был описан выше. vector::emplace_back передает свои параметры конструктору T не зная ничего о том, чем является T.
Следующим шагом рассмотрим несколько примеров, показывающих как можно добиться подобного поведения без использования нововведений С++11. Для упрощения не будем учитывать необходимость использования шаблонов с переменным количеством параметров аргументов, предположим, что требуется передать только два аргумента.
Первый вариант, который приходит на ум:
Но это очевидно не будет работать как нужно, если func принимает параметры по ссылке, так как wrapper принимает параметры по значению. В этом случае, если func изменяет получаемые по ссылке параметры, это не отразится на параметрах, переданных во wrapper (будут изменены копии, созданные внутри wrapper).
Хорошо, тогда мы можем переделать wrapper, чтобы он принимал параметры по ссылке. Это не будет помехой, если func будет принимать не по ссылке, а по значению, так как func внутри wrapper сделает себе необходимые копии.
Здесь другая проблема. Rvalue не может быть передано в функцию в качестве ссылки. Таким образом вполне тривиальный вызов не скомпилируется:
И сразу нет, если пришла мысль сделать эти ссылки константными – это тоже не решит проблему. Потому что func может требовать в качестве параметров неконстантные ссылки.
Остается только грубый подход, используемый в некоторых библиотеках: перегрузить функцию для константных и не неконстантных ссылок:
Экспоненциальный рост. Можно представить, сколько веселья это доставит, когда потребуется обработать какое-то разумное количество параметров реальных функций. Чтобы ухудшить ситуацию С++11 добавляет rvalue ссылки, которые тоже нужно учесть в функции wrapper, и это точно не является расширяемым решением.
Сжатие ссылок и особый вывод типа для rvalue-ссылок
Для объяснения того, как в С++11 реализуется идеальная передача, нужно сначала понять два новых правила, которые были добавлены в этот язык программирования.
Начнем с простого – сжатия ссылок (reference collapsing). Как известно, взятие ссылки на ссылку в С++ не допускается, но это иногда может происходить при реализации шаблонов:
Что случится, если вызвать эту функцию следующим образом:
Не позволяйте двойному амперсанду обмануть Вас – t здесь не является rvalue-ссылкой [2]. При появлении в данной ситуации (когда необходим особый вывод типа), T&& принимает особое значение – когда func инстанцируется, T изменяется в зависимости от переданного типа. Если была передана lvalue типа U, то Т становится U&. Если же U это rvalue, то Т становится просто U. Пример:
Это правило может показаться необычным и даже странным. Оно такое и есть. Но, тем не менее, это правило становится вполне очевидным, когда приходит понимание что это правило помогает решить проблему идеальной передачи.
Реализация идеальной передачи с использованием std::forward
Теперь давайте вернемся к нашей описанной выше шаблонной функции wrapper. Вот как она должна быть реализована с использованием С++11:
А вот как реализован forward [3]:
Рассмотрим следующий вызов:
Рассмотрим первый аргумент (второй аналогичен): ii является lvalue, таким образом T1 становится int& в соответствии с правилом особого вывода типа. Получается вызов func(forward (e1), …). Таким образом, шаблон forward инстанцирован типом int& и получаем следующую версию этой функции:
Время применить правило сжатия ссылок:
Другими словами, аргумент передан по ссылке в func, как и требуется для lvalue.
Следующий пример:
Аргумент, полученный по ссылке, приводится к rvalue-ссылке, которую и требуется получить от forward.
Шаблонную функцию forward можно рассматривать как некоторую обертку над static_cast (t), когда T может принять значение U& или U&&, в зависимости от типа входного аргумента (lvalue или rvalue). Теперь wrapper является одним шаблоном, который обрабатывает любые сочетания типов аргументов.
Шаблонная функция forward реализована в С++11, в заголовочном файле «utility», в пространстве имен std.
Еще один момент, который нужно отметить: использование std::remove_reference. На самом деле forward может быть реализован и без использования этой функции. Сжатие ссылок выполнит всю работу, таким образом, применение std::remove_reference для этого избыточно. Однако, эта функция позволяет вывести T& t в ситуации, когда этот тип не может быть выведен (согласно стандарту С++, 14.8.2.5), поэтому необходимо явно указывать параметры шаблона при вызове std::forward.
Универсальные ссылки
В своих выступлениях, постах в блоге и книгах, Скотт Майерс дает наименование «универсальные ссылки» (universal reference) для rvalue-ссылок, которые в контексте вывода типов. Удачное это наименование или нет, сложно сказать. Что касается меня, когда я первый раз прочитал относящуюся к данной теме главу из новой книги «Effective C++», я почувствовал, что запутался. Более-менее стало все понятно позже, когда я разобрался с лежащими в основе этого механизмами (сжатия ссылок и правил особого вывода типов).
Ловушка заключается в том, что фраза «универсальные ссылки» [4] конечно более кратка и красива, чем «rvalue-ссылки в контексте вывода типов». Но если есть желание на самом деле понять некоторый код, не получится избежать полного описания.
Примеры использования идеальной передачи
Идеальная передача довольно полезна, потому что делает возможным программирование на более высоком уровне. Функции высшего порядка – это функции, которые могут принять другие функции в качестве аргументов или возвращать их. Без идеальной передачи, применение функций высшего порядка довольно обременительно, так как нет удобного способа передать аргументы в функцию внутри функции-обертки. Под термином «функция» я здесь кроме самих функций также имею в виду и классы, конструкторы которых фактически тоже являются функциями.
В начале данной статьи я описывал метод контейнеров emplace_back. Другой хороший пример – это стандартная шаблонная функция make_unique, которую я описывал в предыдущей статье:
Признаюсь честно, что в той статье я просто игнорировал странный двойной амперсанд и фокусировался на переменном количестве аргументов шаблона. Но сейчас совершенно несложно полностью понять код. Само собой разумеется, что идеальная передача и шаблоны с переменным количеством аргументов очень часто используются вместе, потому что, в большинстве случаев неизвестно, какое количество аргументов принимают функция или конструктор, которым мы передаем эти аргументы.
В качестве примера со значительно более сложным использованием идеальной передачи Вы можете посмотреть реализацию std::bind.
Ссылки на источники
От переводчика: на CppCon2014 многими (в том числе Мейерсом, Страуструпом, Саффером) было принято решение использовать термин forwarding references вместо universal references.
std::move vs. std::forward
Несмотря на то, что материалов на тему move-семантики и идеальной передачи в Интернете предостаточно, вопросов типа «что я должен здесь использовать: move или forward?» не становится меньше или мне просто «везет» на них. Поэтому и решено было написать эту статью. Предполагается, что читатель хотя бы немного знаком с rvalue-ссылками, move-семантикой и идеальной передачей.
Для чего нужен шаблон функции std::move?
Функция std::move выполняет приведение передаваемого lvalue-аргумента в rvalue-ссылку. Зачем это нужно? Вернемся во времена С++98 и рассмотрим классический пример:
В этом примере для встроенных типов проблем не возникает, но для таких типов как vector или string копирование может являться крайне дорогой операцией. Необходимо было добавлять специализации для шаблона swap, чтобы выполнять это действие более эффективно, и многих такой подход не устраивал.
Что же здесь нужно изменить? Нам не нужно выполнять копирование, а нужно «перемещать» объекты. Как это сделать? Нужно вызывать специальный конструктор или оператор присваивания, которые не будут копировать содержимое классов, а будут обмениваться им (по сути выполнять swap для всех членов).
Для этого в C++11 ввели rvalue-ссылки, которые обозначаются через && и позволяют ссылаться на временные объекты или определять объекты как «перемещаемые». Также появились конструктор перемещения (move constructor) и оператор перемещающего присваивания (move assignment), которые отличаются от копирующих «коллег» тем, что в качестве аргумента принимают неконстантную rvalue-ссылку:
Теперь операцию swap мы можем переписать с использованием функции std::move, которая возвращает rvalue-ссылку и тем самым сообщает компилятору, что параметр является перемещаемым:
Функция std::move не выполняет никаких перемещений. Как уже было сказано выше, она выполняет приведение типа к rvalue-ссылке. Давайте посмотрим на ее код:
То есть это просто обертка для static_cast, которая «убирает» ссылку у переданного аргумента с помощью remove_reference_t и, добавив &&, преобразует тип в rvalue-ссылку. Давайте глянем на то, как именно remove_reference_t избавляется от ссылок:
Для чего нужен std::forward?
Функция std::forward, как известно, применяется при идеальной передаче (perfect forwarding).
Идеальная передача позволяет создавать функции-обертки, передающие параметры без каких-либо изменений (lvalue передаются как lvalue, а rvalue – как rvalue) и тут std::move нам не подходит, так как она безусловно приводит свой результат к rvalue.
Поэтому, была разработана функция std::forward, которая выполняет примерно следующую работу:
То есть, если ссылка была передана как rvalue, то вызываем духов std::move, а иначе просто возвращаем то, что передали.
Если посмотреть исходники стандартной библиотеки у MS, то мы увидим следующую реализацию:
Может быть немного сложнее, но смысл тот же. И пусть Вас не пугает перегруженная версия forward(remove_reference_t && _Arg). В ней просто добавлена проверка на случай компиляции чудесатых конструкций вроде такой:
На стадии компиляции получим сразу ошибку: bad forward call. И я полагаю, это единственное, ради чего добавлена еще одна сигнатура, поскольку нет никакого смысла городить, например, такие конструкции:
Если у кого-то есть идеи, для чего еще может понадобиться forward(remove_reference_t && _Arg), дайте знать и заранее спасибо.
Возможно сейчас у читателя возникает в голове вопрос: а может на самом деле нет нужды вызывать forward и просто вызывать static_cast (_Arg)? Да можно, конечно, просто forward более безопасен в плане очепяток и путаниц. Кроме того, forward (Arg) в коде выглядит более осмысленным и понятным, нежели static_cast (Arg). Никто же не любит писать комментарии, поэтому лучше писать сразу хорошо читаемый код. То же самое касается и move, и тут придется писать более громоздкую конструкцию:
Пример
Я скопировал код move и forward, вставив в них вывод в cout, чтобы можно было увидеть, что же происходит. Далее мы берем и последовательно вызываем функцию foo, передавая сперва lvalue значение, а затем rvalue. Обратите внимание, что у функции foo аргумент является «универсальной ссылкой» в терминологии Скотта Мейерса, или передаваемой ссылкой (forwarding reference) в терминологии комитета стандартизации С++, то есть она:
Внутри функции foo, вызываем перегруженную функцию bar для константной lvalue-ссылки, для lvalue-ссылки и, наконец, для «универсальной» ссылки.
На выводе получаем:
Итак, разбираем lvalue. В функции foo тип T будет выведен как int&. Почему T выводится как lvalue-сылка, т.е. как int&? По правилам вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано lvalue значение, то T выводится как lvalue-ссылка. Согласно правилам сжатия ссылок (reference collapsing) аргумент p также будет выведен как int&, так как foo(int& && p) превратится в foo(int& p). Таким образом, мы получаем следующую версию foo:
bar(p); вызовет bar(int& v), и тут вопросов возникать не должно.
bar(_move(p)); сперва вызовет move, которая вернет rvalue-ссылку, а значит будет вызвана bar(int&& v).
bar(_forward (p)); сперва вызовет _forward(int& _Arg), которая должна вернуть lvalue-ссылку, и значит будет вызвана опять bar(int& v). Что при этом произойдет внутри _forward? Применив правила сжатия ссылок, мы получим следующую версию _forward:
Разберем теперь rvalue. В функции foo тип T будет выведен как int. Согласно все тех же правил вывода аргумента шаблона для универсальных ссылок, если в качестве аргумента передано rvalue, то T выводится как бессылочный тип. Это необходимо для корректной работы функции forward, как мы это увидим далее. Итак, наша foo выглядит теперь следующим образом:
bar(p); как и ранее, вызовет bar(int& v).
bar(_move(p)); как и ранее, сперва вызовет move, которая вернет rvalue-ссылку, а значит будет опять вызвана bar(int&& v).
Теперь посмотрим как же будет инстанцирована forward :
Давайте подставим вместо move и forward их содержимое, чтобы наглядно посмотреть на разницу:
Вывод: для перемещаемых объектов необходимо использовать std::move, а для идеальной передачи – std::forward.
Примеры использования move и forward
Кроме приведенного выше примера std::swap, использование std:move можно найти в различных алгоритмах, где нужно менять элементы местами (различные сортировки, или, например, в функции std::unique).
Если необходимо «передать» умный указатель std::unique_ptr, то сделать мы можем это только через std::move (либо через release() и сырой указатель, но это не по фень-шую).
В функции std::vector::push_back для rvalue можно обнаружить:
Таким образом, legacy-код, добавляющий новый элемент в вектор через rvalue волшебным образом начинает работать через перемещение, а не копирование.
Если ваша функция возвращает кортеж (или пару), то стоит обратить внимание на возможность перемещения некоторых или даже всех его элементов:
Обратите внимание на то, что не нужно использовать std::move при возврате из функции, возвращающий локальный объект:
Здесь нужно убрать std::move. Всю работу сделает copy/move elision – специальная оптимизация, которую выполняет компилятор, убирая лишние создания объектов.
Функция std::forward и вариативные шаблоны являются фундаментом, на котором строятся такие функции-обертки как std::makeunique, std::makeshared, std::makepair, std::maketuple и другие. Например, make_unique делает очень простую работу:
Семейство emplace методов также работает через forward, зачастую просто вызывая конструктор через placement-new.
Что еще можно почитать на эту тему:
Идеальная передача и универсальные ссылки в C++
Книга Скотта Мейерса «Эффективный и современный С++. 42 рекомендации по использованию C++11 и C++14»