Quels sont les principaux objectifs de l'utilisation de std :: forward et quels problèmes cela résout-il?

438

Dans une transmission parfaite, std::forwardest utilisé pour convertir les références rvalue nommées t1et les t2références rvalue non nommées. Quel est le but de faire cela? Comment cela affecterait-il la fonction appelée innersi nous quittons t1& t2as lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
Steveng
la source

Réponses:

789

Vous devez comprendre le problème de transfert. Vous pouvez lire l'intégralité du problème en détail , mais je vais résumer.

Fondamentalement, étant donné l'expression E(a, b, ... , c), nous voulons que l'expression f(a, b, ... , c)soit équivalente. En C ++ 03, c'est impossible. Il y a de nombreuses tentatives, mais elles échouent toutes à certains égards.


Le plus simple est d'utiliser une référence de valeur:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
    E(a, b, c);
}

Mais cela ne parvient pas à gérer les valeurs temporaires f(1, 2, 3);:, car celles-ci ne peuvent pas être liées à une référence de valeur.

La prochaine tentative pourrait être:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(a, b, c);
}

Ce qui résout le problème ci-dessus, mais renverse les flops. Il ne permet plus maintenant Ed'avoir des arguments non const:

int i = 1, j = 2, k = 3;
void E(int&, int&, int&); f(i, j, k); // oops! E cannot modify these

La troisième tentative accepte les const-références, mais c'est alors const_castle constloin:

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c)
{
    E(const_cast<A&>(a), const_cast<B&>(b), const_cast<C&>(c));
}

Cela accepte toutes les valeurs, peut transmettre toutes les valeurs, mais conduit potentiellement à un comportement non défini:

const int i = 1, j = 2, k = 3;
E(int&, int&, int&); f(i, j, k); // ouch! E can modify a const object!

Une solution finale gère tout correctement ... au prix d'être impossible à maintenir. Vous fournissez des surcharges de f, avec toutes les combinaisons de const et non const:

template <typename A, typename B, typename C>
void f(A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, C& c);

template <typename A, typename B, typename C>
void f(const A& a, B& b, const C& c);

template <typename A, typename B, typename C>
void f(A& a, const B& b, const C& c);

template <typename A, typename B, typename C>
void f(const A& a, const B& b, const C& c);

N arguments nécessitent 2 N combinaisons, un cauchemar. Nous aimerions le faire automatiquement.

(C'est effectivement ce que le compilateur fait pour nous en C ++ 11.)


En C ++ 11, nous avons la possibilité de résoudre ce problème. Une solution modifie les règles de déduction de modèle sur les types existants, mais cela casse potentiellement beaucoup de code. Nous devons donc trouver un autre moyen.

La solution consiste à utiliser à la place les références rvalue nouvellement ajoutées ; nous pouvons introduire de nouvelles règles lors de la déduction des types rvalue-reference et créer tout résultat souhaité. Après tout, nous ne pouvons pas casser le code maintenant.

Si on donne une référence à une référence (note référence est un terme englobant signifiant à la fois T&et T&&), nous utilisons la règle suivante pour déterminer le type résultant:

"[étant donné] un type TR qui est une référence à un type T, une tentative de créer le type" référence lvalue à cv TR "crée le type" référence lvalue à T ", tandis qu'une tentative de créer le type" référence rvalue à cv TR ”crée le type TR."

Ou sous forme de tableau:

TR   R

T&   &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&   && -> T&  // rvalue reference to cv TR -> TR (lvalue reference to T)
T&&  &  -> T&  // lvalue reference to cv TR -> lvalue reference to T
T&&  && -> T&& // rvalue reference to cv TR -> TR (rvalue reference to T)

Ensuite, avec la déduction d'argument modèle: si un argument est une valeur l A, nous fournissons l'argument modèle avec une référence lvalue à A. Sinon, nous déduisons normalement. Cela donne des références dites universelles (le terme référence de transfert est désormais officiel).

Pourquoi est-ce utile? Parce que combinés, nous conservons la possibilité de garder une trace de la catégorie de valeur d'un type: s'il s'agissait d'une lvalue, nous avons un paramètre lvalue-reference, sinon nous avons un paramètre rvalue-reference.

Dans du code:

template <typename T>
void deduce(T&& x); 

int i;
deduce(i); // deduce<int&>(int& &&) -> deduce<int&>(int&)
deduce(1); // deduce<int>(int&&)

La dernière chose est de "transmettre" la catégorie de valeur de la variable. Gardez à l'esprit qu'une fois à l'intérieur de la fonction, le paramètre peut être passé en tant que valeur l à n'importe quoi:

void foo(int&);

template <typename T>
void deduce(T&& x)
{
    foo(x); // fine, foo can refer to x
}

deduce(1); // okay, foo operates on x which has a value of 1

Ce n'est pas bon. E doit obtenir le même type de catégorie de valeur que nous avons! La solution est la suivante:

static_cast<T&&>(x);

Qu'est-ce que cela fait? Considérez que nous sommes à l'intérieur de la deducefonction, et nous avons reçu une valeur l. Cela signifie que Test un A&, et donc le type cible pour la distribution statique est A& &&, ou tout simplement A&. Puisque xc'est déjà un A&, nous ne faisons rien et nous nous retrouvons avec une référence lvalue.

Lorsque nous recevons une valeur r, Tis A, le type cible pour le transtypage statique est A&&. La conversion entraîne une expression rvalue, qui ne peut plus être transmise à une référence lvalue . Nous avons conservé la catégorie de valeur du paramètre.

L'assemblage de ces éléments nous donne une "transmission parfaite":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Quand freçoit une lvalue, Eobtient une lvalue. Quand freçoit une rvalue, Eobtient une rvalue. Parfait.


Et bien sûr, nous voulons nous débarrasser du laid. static_cast<T&&>est cryptique et bizarre à retenir; Faisons plutôt une fonction utilitaire appelée forward, qui fait la même chose:

std::forward<A>(a);
// is the same as
static_cast<A&&>(a);
GManNickG
la source
1
Ne serait-ce pas fune fonction et non une expression?
Michael Foukarakis
1
Votre dernière tentative n'est pas correcte en ce qui concerne l'énoncé du problème: il transmettra les valeurs const comme non const, donc pas du tout de transfert. A noter également qu'à la première tentative, le const int isera accepté: Aest déduit de const int. Les échecs concernent les littéraux rvalues. Notez également que pour l'appel à deduced(1), x est int&&non int(le transfert parfait ne fait jamais de copie, comme ce serait le cas s'il xs'agissait d'un paramètre par valeur). Le simple fait Test int. La raison qui xévalue une valeur l dans le redirecteur est que les références rvalue nommées deviennent des expressions lvalue.
Johannes Schaub - litb
5
Y a-t-il une différence dans l'utilisation forwardou moveici? Ou est-ce juste une différence sémantique?
0x499602D2
28
@David: std::movedoit être appelé sans arguments de modèle explicites et se traduit toujours par une valeur r, tandis que std::forwardpeut finir comme l'un ou l'autre. À utiliser std::movelorsque vous savez que vous n'avez plus besoin de la valeur et que vous souhaitez la déplacer ailleurs, utilisez std::forwardpour cela en fonction des valeurs transmises à votre modèle de fonction.
GManNickG
5
Merci d'avoir commencé par des exemples concrets et d'avoir motivé le problème; très utile!
ShreevatsaR
61

Je pense que d'avoir un code conceptuel implémentant std :: forward peut ajouter à la discussion. Ceci est une diapositive de Scott Meyers parler Un échantillonneur efficace C ++ 11/14

code conceptuel implémentant std :: forward

La fonction movedans le code est std::move. Il y a une implémentation (qui fonctionne) plus tôt dans cet exposé. J'ai trouvé l' implémentation réelle de std :: forward dans libstdc ++ , dans le fichier move.h, mais ce n'est pas du tout instructif.

Du point de vue d'un utilisateur, cela signifie qu'il s'agit std::forwardd'une conversion conditionnelle en une valeur r. Cela peut être utile si j'écris une fonction qui attend soit une lvalue soit une rvalue dans un paramètre et souhaite la passer à une autre fonction en tant que rvalue uniquement si elle a été transmise en tant que rvalue. Si je n'encapsulais pas le paramètre dans std :: forward, il serait toujours passé comme référence normale.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Effectivement, il imprime

std::string& version
std::string&& version

Le code est basé sur un exemple de la conférence mentionnée précédemment. Diapositive 10, vers 15 h 00 depuis le début.

user7610
la source
2
Votre deuxième lien a fini par pointer vers un endroit complètement différent.
Pharap
34

Dans une transmission parfaite, std :: forward est utilisé pour convertir la référence de valeur r nommée t1 et t2 en référence de valeur r non nommée. Quel est le but de faire cela? Comment cela affecterait-il la fonction appelée intérieure si nous laissons t1 et t2 comme valeur l?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Si vous utilisez une référence rvalue nommée dans une expression, il s'agit en fait d'une lvalue (car vous faites référence à l'objet par son nom). Prenons l'exemple suivant:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Maintenant, si on appelle outercomme ça

outer(17,29);

nous aimerions que 17 et 29 soient transmis à # 2 parce que 17 et 29 sont des littéraux entiers et en tant que tels rvalues. Mais puisque t1et t2dans l'expression inner(t1,t2);sont des valeurs l, vous invoquerez # 1 au lieu de # 2. C'est pourquoi nous devons reconvertir les références en références sans nom avec std::forward. Donc, t1in outerest toujours une expression lvalue tandis que forward<T1>(t1)peut être une expression rvalue selon T1. Cette dernière n'est qu'une expression lvalue si T1est une référence lvalue. Et T1n'est déduit que pour être une référence lvalue dans le cas où le premier argument à external était une expression lvalue.

sellibitze
la source
C'est une sorte d'explication édulcorée, mais une explication très bien faite et fonctionnelle. Les gens devraient lire cette réponse en premier et ensuite approfondir s'ils le souhaitent
NicoBerrogorry
@sellibitze Une autre question, quelle affirmation est correcte lors de la déduction de int a; f (a): "puisque a est une valeur l, donc int (T &&) équivaut à int (int & &&)" ou "pour que T && soit égal à int &, donc T devrait être int & "? Je préfère celui-ci.
John
11

Comment cela affecterait-il la fonction appelée intérieure si nous laissons t1 et t2 comme valeur l?

Si, après instanciation, T1est de type charet T2appartient à une classe, vous souhaitez passer t1par copie et t2par constréférence. Eh bien, à moins de les inner()prendre par non- constréférence, c'est-à-dire dans ce cas, vous voulez le faire aussi.

Essayez d'écrire un ensemble de outer()fonctions qui implémentent cela sans références rvalue, en déduisant la bonne façon de passer les arguments du inner()type de. Je pense que vous aurez besoin de quelque chose 2 ^ 2 d'entre eux, des trucs de méta-modèle assez lourds pour déduire les arguments, et beaucoup de temps pour bien faire les choses dans tous les cas.

Et puis quelqu'un arrive avec un inner()qui prend des arguments par pointeur. Je pense que cela fait maintenant 3 ^ 2. (Ou 4 ^ 2. Enfer, je ne peux pas être dérangé pour essayer de penser si le constpointeur ferait une différence.)

Et imaginez ensuite que vous voulez le faire pour cinq paramètres. Ou sept.

Vous savez maintenant pourquoi certains esprits brillants ont proposé un «transfert parfait»: cela fait que le compilateur fait tout cela pour vous.

sbi
la source
5

Un point qui n'a pas été rendu clair est que cela se static_cast<T&&>gère const T&correctement aussi.
Programme:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produit:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Notez que «f» doit être une fonction de modèle. S'il est juste défini comme 'void f (int && a)' cela ne fonctionne pas.

lalawawa
la source
bon point, donc T&& dans la distribution statique est également conforme aux règles de réduction de référence, non?
barney
3

Il peut être utile de souligner que le renvoi doit être utilisé en tandem avec une méthode externe avec renvoi / référence universelle. L'utilisation de l'avant par lui-même comme les déclarations suivantes est autorisée, mais ne sert à rien d'autre que de semer la confusion. Le comité standard peut vouloir désactiver une telle flexibilité, sinon pourquoi ne pas simplement utiliser static_cast à la place?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

À mon avis, avancer et avancer sont des modèles de conception qui sont des résultats naturels après l'introduction du type de référence de valeur r. Nous ne devons pas nommer une méthode en supposant qu'elle est correctement utilisée, sauf si une utilisation incorrecte est interdite.

colin
la source
Je ne pense pas que le comité C ++ estime qu'il leur incombe d'utiliser les idiomes du langage "correctement", ni même de définir ce qu'est une utilisation "correcte" (bien qu'ils puissent certainement donner des directives). À cette fin, alors que les enseignants, les patrons et les amis d'une personne peuvent avoir le devoir de les diriger d'une manière ou d'une autre, je pense que le comité C ++ (et donc la norme) n'a pas ce devoir.
SirGuy
Ouais, je viens de lire N2951 et je suis d'accord que le comité standard n'a aucune obligation d'ajouter des limitations inutiles concernant l'utilisation d'une fonction. Mais les noms de ces deux modèles de fonctions (déplacer et avancer) sont en effet un peu déroutants en ne voyant que leurs définitions dans le fichier de bibliothèque ou la documentation standard (23.2.5 Assistants de déplacement / déplacement). Les exemples de la norme aident certainement à comprendre le concept, mais il pourrait être utile d'ajouter plus de remarques pour rendre les choses un peu plus claires.
colin