Зачем использовать 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
должна вести себя "правильно". Это были
- Следует переслать lvalue как lvalue
- Следует переслать rvalue как rvalue
- Не следует пересылать значение как значение
- Должны пересылать менее квалифицированные cv выражения в более квалифицированные cv выражения
- Должен ли перенаправлять выражения производного типа в доступный однозначный базовый тип
- Не следует пересылать произвольные преобразования типов
(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
.