Зачем использовать std :: forward <T>вместо static_cast <T &&> - PullRequest
0 голосов
/ 12 ноября 2018

При заданном коде следующей структуры

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

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


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

Ответы [ 3 ]

0 голосов
/ 12 ноября 2018

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

Пункт 23: Понять std :: move и 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.

0 голосов
/ 12 ноября 2018

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 во избежание создания экземпляров - это потеря времени по сравнению с преимуществом использования более выразительного (и более безопасного) кода. Взрыв экземпляра шаблона более эффективно управляется на уровне архитектуры кода.

0 голосов
/ 12 ноября 2018

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...