Как вывод типов для forwarding reference позволяет шаблонной функции сохранить категорию переданного аргумента?
Если параметр имеет форму T&&, где T выводится и не является параметром класса, это forwarding reference. Для lvalue выводится ссылочный тип T&, после схлопывания ссылок параметр становится lvalue-ссылкой; для rvalue выводится обычный T, и параметр остаётся rvalue-ссылкой. Вызов std::forward<T> восстанавливает исходную категорию значения при передаче дальше.
До появления rvalue-ссылок в C++11 обобщённые оболочки часто теряли информацию о том, был ли аргумент временным объектом или именованным объектом. Это мешало эффективно перемещать временные значения и заставляло писать множество перегрузок.
Perfect forwarding объединил вывод типов, схлопывание ссылок и rvalue-ссылки. Такой механизм позволяет обёртке передавать аргумент дальше почти так, как если бы промежуточной функции не существовало.
Именованный параметр внутри функции всегда является lvalue-выражением, даже если его тип — rvalue-ссылка. Поэтому простая передача параметра дальше без std::forward превращает исходный rvalue в lvalue.
Неверное решение может выбрать не ту перегрузку, отключить перемещение и привести к лишнему копированию. При этом безусловное применение std::move также опасно: оно превращает lvalue в rvalue и может некорректно переместить объект, которым вызывающий код ещё рассчитывает пользоваться.
Для forwarding reference действуют следующие правила:
X, T выводится как X&; тип параметра T&& после схлопывания ссылок становится X&;X, T выводится как X; параметр имеет тип X&&;const, поэтому изменять объект через параметр нельзя.Схлопывание ссылок подчиняется правилам, где комбинация с lvalue-ссылкой даёт lvalue-ссылку: X& && превращается в X&, а X&& && — в X&&. Именно поэтому вывод T = X& для исходного lvalue сохраняет его категорию.
std::forward<T>(value) условно преобразует параметр обратно в lvalue или rvalue в зависимости от выведенного T. Само наличие T&& не выполняет forwarding автоматически: без std::forward параметр используется как именованное lvalue.
Forwarding reference существует не для любого T&&. Например, параметр с уже известным типом Widget&& не использует вывод типов и является обычной rvalue-ссылкой. Аналогично, T&& в некоторых контекстах, где T не выводится непосредственно из аргумента, не обладает свойствами forwarding reference.
Вызов relay(n) выводит T как int&; после схлопывания параметр и результат std::forward<T> являются lvalue. Вызов relay(0) выводит T как int, поэтому forwarding передаёт rvalue.
Главный компромисс — усложнение интерфейса и диагностики ошибок. Шаблонная обёртка может принимать слишком широкий набор типов, а ошибки внутри тела становятся менее очевидными; поэтому forwarding следует применять там, где действительно нужно сохранить свойства исходного аргумента.
В библиотеке есть функция-обёртка, которая логирует вызов и затем передаёт объект в конструктор. Для временных объектов желательно сохранить возможность перемещения, а для lvalue — не разрушать исходный объект.
Вариант с передачей параметра по значению прост: он может потребовать копирования lvalue и не всегда точно выражает намерение интерфейса. Набор перегрузок для lvalue и rvalue даёт ясное поведение, но увеличивает код и плохо масштабируется при нескольких параметрах.
Вариант с безусловным std::move сохраняет эффективность для временных объектов, но ошибочно трактует lvalue как объект, который можно переместить. Выбран forwarding reference вместе с std::forward: lvalue передаются без принудительного перемещения, а временные значения сохраняют возможность перемещения.
Результат — одна обобщённая обёртка с корректным выбором перегрузок. При этом команда отдельно ограничивает область применения шаблона и проверяет, что вызываемый объект действительно поддерживает все допустимые категории аргументов.
Почему параметр forwarding reference не теряет исходный тип, хотя внутри функции он является lvalue?
Потому что здесь участвуют два разных свойства. Именованное выражение параметра действительно является lvalue, но его объявленный тип сохраняет результат вывода; std::forward<T> использует этот результат, чтобы восстановить исходную категорию. Если передать параметр дальше напрямую, сохранение не произойдёт.
Чем forwarding reference отличается от обычной rvalue-ссылки?
Обычная rvalue-ссылка имеет заранее заданный тип, например Widget&&, и принимает только rvalue, кроме специальных правил для совместимости с квалификаторами в других контекстах. Forwarding reference содержит непосредственно выводимый параметр шаблона в форме T&&, поэтому может связаться и с lvalue, и с rvalue. Наличие двух амперсандов само по себе не означает forwarding reference.
Почему нельзя всегда заменить std::forward<T>(value) на std::move(value)?
std::move безусловно приводит выражение к xvalue, поэтому lvalue-аргумент тоже будет передан как потенциально перемещаемый объект. Это может изменить состояние объекта вызывающего кода и выбрать неподходящую перегрузку. std::forward<T> условен: он выполняет преобразование в rvalue только тогда, когда исходный аргумент был выведен как rvalue.