Зачем использовать std :: forward <T> вместо static_cast <T &&>

Когда дан код следующей структуры

template <typename... Args>
void foo(Args&&... args) { ... }

Я часто видел, как библиотечный код использует static_cast<Args&&> внутри функции для пересылки аргументов. Как правило, это оправдывается тем, что использование static_cast позволяет избежать ненужного создания экземпляра шаблона.

С учетом свертывания языковой ссылки и правил вывода шаблонов. Мы получаем идеальную пересылку с static_cast<Args&&>, доказательство для этого утверждения ниже (в пределах полей ошибок, которые, я надеюсь, ответ просветит)

  • Когда даны ссылки на rvalue (или для полноты - нет ссылки на квалификации, как в этом примере), это сворачивает ссылки таким образом, что результатом является rvalue. Используется правило && &&&& (правило 1 выше)
  • Когда даны ссылки на lvalue, это сворачивает ссылки таким образом, что результатом является lvalue. Здесь используется правило & &&& (правило 2 выше)

По сути, это заставляет foo() пересылать аргументы в bar() в приведенном выше примере. Это поведение, которое вы получите при использовании std::forward<Args> здесь.


Вопрос - зачем вообще использовать std::forward в этих контекстах? Оправдывает ли отказ от дополнительной инстанциации нарушение соглашения?

В статье Говарда Хиннанта n2951 указано 6 ограничений, при которых любая реализация std::forward должна вести себя "правильно". Это были

  1. Следует переслать lvalue как lvalue
  2. Следует переслать rvalue как rvalue
  3. Не следует пересылать значение как значение
  4. Должны пересылать менее квалифицированные cv выражения в более квалифицированные cv выражения
  5. Должен ли перенаправлять выражения производного типа в доступный однозначный базовый тип
  6. Не следует пересылать произвольные преобразования типов

(1) и (2) было доказано, что правильно работает со static_cast<Args&&> выше. (3) - (6) здесь не применимы, потому что когда функции вызываются в выведенном контексте, ничего из этого не может произойти.


Примечание: я лично предпочитаю использовать std::forward, но у меня есть оправдание только потому, что я предпочитаю придерживаться соглашения.

Ответ 1

forward выражает намерение, и это может быть более безопасно для использования, чем static_cast: static_cast рассматривает преобразование, но некоторые опасные и предположительно неумышленные преобразования обнаруживаются с помощью forward:

struct A{
   A(int);
   };

template<class Arg1,class Arg2>
Arg1&& f(Arg1&& a1,Arg2&& a2){
   return static_cast<Arg1&&>(a2); //  typing error: a1=>a2
   }

template<class Arg1,class Arg2>
Arg1&& g(Arg1&& a1,Arg2&& a2){
   return forward<Arg1>(a2); //  typing error: a1=>a2
   }

void test(const A a,int i){
   const A& x = f(a,i);//dangling reference
   const A& y = g(a,i);//compilation error
  }

Пример сообщения об ошибке: ссылка на компилятор


Как применяется это обоснование: как правило, это оправдывается тем, что использование static_cast позволяет избежать ненужного создания экземпляра шаблона.

Является ли время компиляции более проблематичным, чем возможность сопровождения кода? Должен ли кодер даже терять время на минимизацию "ненужных реализаций шаблонов" в каждой строке кода?

Когда создается экземпляр шаблона, его создание вызывает создание экземпляров шаблона, которые используются в его определении и объявлении. Так что, например, если у вас есть функция как:

  template<class T> void foo(T i){
     foo_1(i),foo_2(i),foo_3(i);
     }

где foo_1, foo_2, foo_3 являются шаблонами, создание экземпляра foo вызовет 3 экземпляра. Затем рекурсивно, если эти функции вызывают создание экземпляров 3 других шаблонных функций, вы можете получить, например, 3 * 3 = 9 экземпляров. Таким образом, вы можете рассматривать эту цепочку инстанцирования как дерево, в котором инстанцирование корневой функции может вызывать тысячи инстанциаций как экспоненциально растущий волновой эффект. С другой стороны, такая функция, как forward является листом в этом дереве реализации. Поэтому, избегая его создания, можно избежать только 1 создания.

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

Так что, по моему мнению, использование static_cast вместо " forward позволяет избежать затрат времени по сравнению с преимуществом использования более выразительного (и более безопасного) кода. Взрыв экземпляра шаблона более эффективно управляется на уровне архитектуры кода.

Ответ 2

Скотт Мейерс говорит, что std::forward и std::move в основном для удобства. Он даже утверждает, что std::forward может использоваться для выполнения функций как std::forward и std::move.
Некоторые выдержки из "Эффективного Модерна C++":

Пункт 23: Понять std :: move и std :: forward
...
История для std::forward аналогична истории для std::forward std::move, но в то время как std::move безоговорочно приводит свой аргумент к rvalue, std::forward делает это только при определенных условиях. std::forward - это условное приведение Он приводит к rvalue только если его аргумент был инициализирован с помощью rvalue.
...
Учитывая, что как std::move и std::forward сводятся к приведению, единственное отличие состоит в том, что std::move всегда приводит к приведению, а std::forward только иногда делает это, вы можете спросить, можем ли мы отказаться от std::move и просто используйте std::forward везде. С чисто технической точки зрения ответ - да: std::forward может все это сделать. std::move не требуется. Конечно, ни одна из этих функций на самом деле не нужна, потому что мы могли бы писать приведения повсюду повсюду, но я надеюсь, что мы согласимся, что это будет, ну, в общем, отвратительно
...
Достопримечательности std::move - удобство, уменьшенная вероятность ошибки и большая ясность...

Для тех, кому интересно, сравнение std::forward<T> и static_cast<T&&> в сборке (без какой-либо оптимизации) при вызове с lvalue и rvalue.

Ответ 3

Вот мои 2.5, не очень технические центы для этого: тот факт, что сегодня std::forward действительно является простым старым static_cast<T&&>, не означает, что завтра он также будет реализован точно таким же образом. Я думаю, что комитету нужно что-то, чтобы отразить желаемое поведение того, что std::forward достигает сегодня, следовательно, появился forward который нигде ничего не передает.

Имея необходимое поведение и ожидания, формализованные под эгидой std::forward, теоретически говоря, никто не мешает будущему исполнителю предоставлять std::forward не как static_cast<T&&> а что-то конкретное для его собственной реализации, фактически не принимая во внимание static_cast<T&&> потому что единственный факт, который имеет значение, это правильное использование и поведение std::forward.