web-dev-qa-db-fra.com

Pourquoi utiliser std :: forward <T> au lieu de static_cast <T &&>

Lorsque le code de la structure suivante est donné

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

J'ai souvent vu du code de bibliothèque utiliser static_cast<Args&&> Dans la fonction pour le transfert d'arguments. En règle générale, la justification est que l'utilisation d'un static_cast Évite une instanciation de modèle inutile.

Compte tenu de l'effondrement des références du langage et des règles de déduction du modèle. Nous obtenons une transmission parfaite avec le static_cast<Args&&>, La preuve de cette réclamation est ci-dessous (dans les marges d'erreur, que j'espère qu'une réponse éclairera)

  • Lorsqu'on donne des références rvalue (ou pour être complet - aucune qualification de référence comme dans cet exemple ), cela réduit les références de telle manière que le résultat est une rvalue. La règle utilisée est && && -> && (Règle 1 Ci-dessus)
  • Lorsqu'on donne des références lvalue, cela réduit les références de telle manière que le résultat est une lvalue. La règle utilisée ici est & && -> & (Règle 2 Ci-dessus)

Il s'agit essentiellement d'obtenir foo() pour transmettre les arguments à bar() dans l'exemple ci-dessus. C'est le comportement que vous obtiendriez également en utilisant std::forward<Args> Ici.


Question - pourquoi utiliser std::forward Dans ces contextes? Éviter l'instanciation supplémentaire justifie-t-il de rompre la convention?

L'article de Howard Hinnant n2951 spécifiait 6 contraintes sous lesquelles toute implémentation de std::forward Devrait se comporter "correctement". C'étaient

  1. Doit transmettre une lvalue en tant que lvalue
  2. Doit transmettre une rvalue comme une rvalue
  3. Ne doit pas transmettre une valeur r en tant que valeur l
  4. Devrait transférer moins d'expressions qualifiées cv vers des expressions plus qualifiées cv
  5. Doit transmettre des expressions de type dérivé à un type de base accessible et sans ambiguïté
  6. Ne doit pas transmettre des conversions de types arbitraires

(1) et (2) se sont avérés fonctionner correctement avec static_cast<Args&&> Ci-dessus. (3) - (6) ne s'appliquent pas ici car lorsque des fonctions sont appelées dans un contexte déduit, aucune de celles-ci ne peut se produire.


Remarque: Personnellement, je préfère utiliser std::forward, Mais la justification que j'ai est purement que je préfère m'en tenir à la convention.

23
Curious

forward exprime l'intention et il peut être plus sûr à utiliser que static_cast: static_cast considère la conversion, mais certaines conversions dangereuses et supposément non intentionnelles sont détectées avec 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
  }

Exemple de message d'erreur: lien Explorateur du compilateur


Comment applique cette justification: En règle générale, la justification est que l'utilisation d'un static_cast évite une instanciation de modèle inutile.

Le temps de compilation est-il plus problématique que la maintenabilité du code? Le codeur devrait-il même perdre son temps en envisageant de minimiser "l'instanciation de modèle inutile" à chaque ligne du code?

Lorsqu'un modèle est instancié, son instanciation provoque des instanciations de modèle utilisées dans sa définition et sa déclaration. Ainsi, par exemple si vous avez une fonction comme:

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

foo_1, foo_2, foo_3 sont des modèles, l'instanciation de foo provoquera 3 instanciations. Ensuite, récursivement, si ces fonctions provoquent l'instanciation d'autres 3 fonctions de modèle, vous pouvez obtenir 3 * 3 = 9 instanciations par exemple. Vous pouvez donc considérer cette chaîne d'instanciation comme un arbre où une instanciation de la fonction racine peut provoquer des milliers d'instanciations comme un effet d'entraînement exponentiellement croissant. D'un autre côté, une fonction comme forward est une feuille dans cet arbre d'instanciation. Ainsi, éviter son instanciation ne peut éviter qu'une seule instanciation.

Ainsi, la meilleure façon d'éviter l'explosion d'instanciation de modèle est d'utiliser le polymorphisme dynamique pour les classes "racine" et le type d'argument des fonctions "racine", puis d'utiliser le polymorphisme statique uniquement pour les fonctions critiques en termes de temps qui sont pratiquement supérieures dans cet arbre d'instanciation.

Donc, à mon avis, en utilisant static_cast en place forward pour éviter les instanciations est une perte de temps par rapport à l'avantage d'utiliser un code plus expressif (et plus sûr). L'explosion d'instanciation de modèle est gérée plus efficacement au niveau de l'architecture de code.

7
Oliv

Scott Meyers dit que std::forward Et std::move Sont principalement pour des raisons de commodité. Il déclare même que std::forward Peut être utilisé pour exécuter les fonctionnalités de std::forward Et de std::move.
Quelques extraits de "Effective Modern C++":

Point 23: Comprendre std :: move et std :: forward
...
L'histoire de std::forward Est similaire à celle de std::move, Mais alors que std::move Convertit inconditionnellement son argument en rvalue, std::forward Ne le fait que sous certaines conditions. std::forward Est une distribution conditionnelle. Il ne convertit en rvalue que si son argument a été initialisé avec un rvalue.
...
Étant donné que std::move Et std::forward Se résument à des lancers, la seule différence étant que std::move Fait toujours des lancers, alors que std::forward Ne , vous pourriez vous demander si nous pouvons nous passer de std::move et utiliser simplement std::forward partout. D'un point de vue purement technique, la réponse est oui: std::forward Peut tout faire. std::move N'est pas nécessaire. Bien sûr, aucune fonction n'est vraiment nécessaire, car nous pourrions écrire des transtypages partout, mais j'espère que nous conviendrons que ce serait, eh bien, dégoûtant.
...
Les attractions de std::move Sont la commodité, la probabilité d'erreur réduite et une plus grande clarté ...

Pour ceux qui sont intéressés, comparaison de std::forward<T> Vs static_cast<T&&> Dans Assembly (sans aucune optimisation) lorsqu'il est appelé avec lvalue et rvalue.

13
P.W

Voici mes 2,5 cents, pas si techniques pour cela: le fait qu'aujourd'hui std::forward n'est en effet qu'un simple vieux static_cast<T&&> ne signifie pas que demain, il sera également mis en œuvre exactement de la même manière. Je pense que le comité avait besoin de quelque chose pour refléter le comportement souhaité de ce qui std::forward atteint aujourd'hui d'où le forward qui ne transmet rien nulle part a vu le jour.

Avec le comportement et les attentes requis formalisés sous l'égide de std::forward, en théorie, personne n'empêche un futur implémenteur de fournir le std::forward pas le static_cast<T&&> mais quelque chose de spécifique à sa propre implémentation, sans vraiment prendre en compte static_cast<T&&> parce que le seul fait qui compte est l'utilisation et le comportement corrects de std::forward.

5
Ferenc Deak