Найти в Дзене
ModernCode

Идеальное перемещение в C++ (Perfect forwarding)

Оглавление

Rvalue - ссылки, семантика перемещений и прямая передача.

Прежде чем начать рассматривать данную тему, вспомним базовые определения и конструкции языка С++.

Ссылка - это тип переменной в С++, который работает как псевдоним другого объекта или значения. Ссылки бывают на константные и неконстантные значения и на rvalue.

lvalue (locator value) - объект, которые занимает идентифицируемое место в памяти и имеет адрес.

Шаблоны (запись вида: template<typename T>) - это механизм, предназначенный для кодирования обобщенных алгоритмов, без привязки к типу данных.

decltype - позволяет статически определить тип по типу другой переменной.

Семантика перемещения предоставляет возможность заменять дорогостоящие операции копирования , благодаря чему ваша программа будет потреблять меньше ресурсов. Она также позволяет создавать типы, которые могут только перемещаться, такие как std::unique_ptr, std::future и другие.

Прямая передача позволяет писать шаблоны функций, которые принимают произвольные аргументы и передают их другим функциям . Таким образом, конечные функции получают в точности те же аргументы, что и переданные исходным.

Rvalue - ссылки представляют собой механизм, который объединяет семантику перемещения и прямую передачу и позволяет использовать их в связке.

Эти понятия имеют очень много нюансов, которые далее мы будем рассматривать.

std::move и std::forward.

std::move всегда выполняет приведение своего аргумента к rvalue.

std::forward выполняет приведение только по соблюдению определенных условий.

Легче всего разобраться с ними с точки зрения того, что они не делают.

Отметим, что std::move ничего не перемещает, а std::forward ничего не передает. В результате вызова эти функции не производят действия, которым требуется время на выполнение, в том числе не генерируют исполняемый код. Следовательно, являются всего лишь шаблонами функций, которые выполняют приведение типов.

Рассмотрим пример реализации std::move в С++11:

Реализация std::move в С++11
Реализация std::move в С++11

Обратим внимание, std::move получает и возвращает универсальную ссылку на один и тот же объект. Часть && возвращаемого типа функции предполагает, что std::move вернет rvalue. Но, если тип T является lvalue, T&& приводится к lvalue. Чтобы этого не произошло, к T применяется свойство std::remove_reference, обеспечивая применение && к типу, не являющемуся ссылкой. Это гарантирует, что std::move возвращает rvalue. Таким образом, std::move приводит свой аргумент к rvalue.

В C++14 реализация std::move упрощается благодаря выводу возвращаемого типа функции и шаблону псевдонима std::remove_reference_t:

-2

Рассмотрим пример:

-3

На самом деле, этот код не делает то, что от него ожидается, но при этом компилируется, собирается и выполняется. Он устанавливает значение str равным содержимому строки text. Данный случай не является реализацией идеального перемещения, потому что text копируется в str. Можно подумать, что text приводится к rvalue с помощью std::move. Но перед приведением text объявляется как const std::string и является lvalue типа const std::string. Результатом приведения является rvalue типа const std::string, и на протяжении всех этих действий константность сохраняется.

Рассмотрим, как компиляторы определяют, какой из конструкторов std::string должен быть вызван:

-4

В списке инициализации членов конструктора Test1 результатом std::move(text) является rvalue типа const std::string. Этот rvalue нельзя передать конструктору перемещения std::string, потому что он получает rvalue на неконстантную std::string. Но rvalue может быть передан конструктору копирования, так как lvalue на const можно связывать с константным lvalue. Такое поведение программы имеет огромное значение для поддержания правильности const. Перемещение значения из объекта модифицирует его, так что язык программирования не должен разрешать передавать константные объекты в функции, которые могут их модифицировать.

Из этого можно сделать два важных вывода:

  1. Если вы планируете перемещать эти объекты,не объявляйте их константными.
  2. std::move не гарантирует, что приведенный этой функцией объект будет перемещен.

Ситуация с std::forward подобна std::move.

std::forward является условным приведением.

В функции foo parametr передается функции f, перегруженной для lvalue и rvalue. Вызывая foo с lvalue, мы ожидаем, что lvalue будет передано функции f как lvalue, а вызывая foo с rvalue - как rvalue. Однако parametr, как и все параметры функций, является lvalue. Каждый вызов внутри foo будет вызывать перегрузку f для lvalue. Для предотвращения такого поведения нам нужен механизм приведения parametr к rvalue, тогда и только тогда, когда аргумент, которым инициализируется parametr - аргумент переданный foo - был rvalue. Именно этим и занимается std::forward, представляющий собой условное приведение и выполняющий приведение к rvalue только тогда, когда аргумент инициализирован как rvalue.

Исходя из этого можно сделать следующие выводы:

  1. std::move выполняет безусловное приведение к rvalue. Сама по себе функция ничего не перемещает.

2. std::forward приводит свой аргумент к rvalue только тогда, когда аргумент связан с rvalue.

3. std::move и std::forward не производят никаких действий, которым требуется время на выполнение.

Наука
7 млн интересуются