С функцией move вроде бы всё понятно: она приводит любую ссылку на объект к неконстантной rvalue-ссылке, которая в соответствующем контексте передастся либо конструктору перемещения, либо оператору присваивания перемещения. Но я иногда встречаю в коде на C++ вызов функции forward в таком же контексте, где я бы использовал move. То, что можно нагуглить про эту функцию, понять не получается. Что она делает?
Функция forward, как и функция move, выполняет приведение аргумента к rvalue. Отличие ее от move в том, что приведение она выполняет условное - в зависимости от аргумента результатом forward может быть как rvalue, так и lvalue. Используется forward в идиоматической конструкции прямой передачи (perfect forwarding), а выглядит эта идиома так:
template <typename T>
void f(T&& arg) {
. . .
g(std::forward<T>(arg));
}
Суть этой идиомы в том, что если ссылка на шаблонизированный тип имеет строго вид T&&, и если в процессе вывода типа компилятор выполняет свертывание ссылок, то в результате свертывания результирующая ссылка может быть выведена как rvalue, так и lvalue. Использовать move в таком случае нельзя, так как программист, вызывающий функцию f с обычной переменной в качестве аргумента, может не подозревать, что ее аргумент исчезнет. Тогда как forward в таком случае корректно передаст аргумент в g как lvalue.
В стандарте такая ссылка называется "передаваемая ссылка" (forwarding reference), но с подачи Скотта Мейерса используется и название "универсальная ссылка" (universal reference).
Шаблон может быть и неявным, например:
int j = 0;
auto&& r = j; // Вывод типа происходит, так что r - передаваемая ссылка.
Ситуация с auto&& типична для лямбда функций, так что там тоже надо употреблять move с осторожностью.
Простой пример работы forward и move
std::string gstr;
template<typename T>
void g(T arg) {
gstr = std::move(arg);
}
template<typename T>
void f(T&& arg) {
g(std::move(arg));
}
int main() {
std::string str { "test" };
f(str);
После выполнения f(str) строка "test" перемещается в gstr, чего программист, вероятно, не ожидал. Но вот если вызов g записать как g(std::forward<T>(arg)), то функция forward распознает аргумент arg как lvalue и строка корректно скопируется.
forward нужен для того, чтобы пробрасывать изначальный тип дальше по цепочке. Отсюда его имя и единственное назначение. Вот пример:
template<typename... Args>
void secondLevel(Args&& ...args)
{
...
}
template<typename... Args>
void firstLevel(Args&& ...args)
{
secondLevel(std::forward<Args>(args)...);
}
Используя пробравсывающие ссылки, мы получаем «истинный» тип переданного нам выражения, а с помощью forward мы сохраняем этот же тип для функции secondLevel неизменным.
В этой статье я приводил ещё один пример.
Основные этапы разработки сайта для стоматологической клиники
Продвижение своими сайтами как стратегия роста и независимости