web-dev-qa-db-fra.com

Comment fonctionne std :: forward?

Duplicate possible:
Avantages de l’utilisation de Forward

Je sais ce que ça fait et quand l'utiliser, mais je toujours ne peux pas comprendre comment cela fonctionne. Veuillez être aussi détaillé que possible et expliquer quand std::forward serait inexact s'il était autorisé à utiliser la déduction d'argument de modèle.

Une partie de ma confusion est la suivante: "S'il a un nom, c'est une valeur" - si c'est le cas, pourquoi std::forward se comporte différemment quand je passe thing&& x contre thing& x?

102
David

Voyons d’abord ce que std::forward Fait selon la norme:

§20.2.3 [forward] p2

Retourne: static_cast<T&&>(t)

(Où T est le paramètre de modèle spécifié explicitement et t est l'argument transmis.)

Maintenant, rappelez-vous les règles de réduction de référence:

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)

(Volé sans vergogne de cette réponse .)

Et ensuite, jetons un coup d'œil à une classe qui souhaite utiliser la transmission parfaite:

template<class T>
struct some_struct{
  T _v;
  template<class U>
  some_struct(U&& v)
    : _v(static_cast<U&&>(v)) {} // perfect forwarding here
                                 // std::forward is just syntactic sugar for this
};

Et maintenant, un exemple d'invocation:

int main(){
  some_struct<int> s1(5);
  // in ctor: '5' is rvalue (int&&), so 'U' is deduced as 'int', giving 'int&&'
  // ctor after deduction: 'some_struct(int&& v)' ('U' == 'int')
  // with rvalue reference 'v' bound to rvalue '5'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int&&>(v)'
  // this just turns 'v' back into an rvalue
  // (named rvalue references, 'v' in this case, are lvalues)
  // huzzah, we forwarded an rvalue to the constructor of '_v'!

  // attention, real magic happens here
  int i = 5;
  some_struct<int> s2(i);
  // in ctor: 'i' is an lvalue ('int&'), so 'U' is deduced as 'int&', giving 'int& &&'
  // applying the reference collapsing rules yields 'int&' (& + && -> &)
  // ctor after deduction and collapsing: 'some_struct(int& v)' ('U' == 'int&')
  // with lvalue reference 'v' bound to lvalue 'i'
  // now we 'static_cast' 'v' to 'U&&', giving 'static_cast<int& &&>(v)'
  // after collapsing rules: 'static_cast<int&>(v)'
  // this is a no-op, 'v' is already 'int&'
  // huzzah, we forwarded an lvalue to the constructor of '_v'!
}

J'espère que cette réponse étape par étape vous aidera, vous et les autres, à comprendre le fonctionnement de std::forward.

137
Xeo

Je pense que l'explication de std::forward Comme static_cast<T&&> Est déroutante. Notre intuition pour un transtypage est de convertir un type en un autre type - dans ce cas, il s'agirait d'une conversion en une référence rvalue. Ce n'est pas! Nous expliquons donc une chose mystérieuse en utilisant une autre chose mystérieuse. Cette distribution particulière est définie par une table dans la réponse de Xeo. Mais la question est pourquoi? Alors voici ma compréhension:

Supposons que je veuille vous transmettre un std::vector<T> v Que vous êtes censé stocker dans votre structure de données en tant que membre de données _v. La solution naïve (et sûre) consisterait à toujours copier le vecteur dans sa destination finale. Donc, si vous le faites via une fonction intermédiaire (méthode), cette fonction doit être déclarée comme prenant une référence. (Si vous le déclarez comme prenant un vecteur par valeur, vous effectuerez une copie supplémentaire totalement inutile.)

void set(std::vector<T> & v) { _v = v; }

Tout va bien si vous avez une lvalue dans la main, mais qu'en est-il une valeur? Supposons que le vecteur soit le résultat de l'appel d'une fonction makeAndFillVector(). Si vous avez effectué une mission directe:

_v = makeAndFillVector();

le compilateur aurait déplacer le vecteur plutôt que de le copier. Mais si vous introduisez un intermédiaire, set(), les informations sur la nature de la valeur de votre argument seront perdues et une copie sera réalisée.

set(makeAndFillVector()); // set will still make a copy

Afin d'éviter cette copie, vous avez besoin d'un "transfert parfait", qui aboutirait à un code optimal à chaque fois. Si vous recevez une lvalue, vous voulez que votre fonction la traite comme une lvalue et en fasse une copie. Si une valeur est définie, vous souhaitez que votre fonction la traite comme une valeur et la déplace.

Normalement, vous surchargeriez la fonction set() séparément pour les valeurs et les valeurs:

set(std::vector<T> & lv) { _v = v; }
set(std::vector<T> && rv) { _v = std::move(rv); }

Mais maintenant, imaginez que vous écrivez une fonction modèle qui accepte T et appelle set() avec ce T (ne vous inquiétez pas du fait que notre set() n'est défini que pour les vecteurs). Le truc, c'est que vous voulez que ce modèle appelle la première version de set() lorsque la fonction de modèle est instanciée avec une valeur lvalue, et la seconde lorsqu'elle est initialisée avec une valeur rvalue.

Tout d’abord, quelle doit être la signature de cette fonction? La réponse est la suivante:

template<class T>
void perfectSet(T && t);

Selon la façon dont vous appelez cette fonction de modèle, le type T sera déduit de manière magique. Si vous appelez cela avec une lvalue:

std::vector<T> v;
perfectSet(v);

le vecteur v sera passé par référence. Mais si vous appelez cela avec une valeur:

perfectSet(makeAndFillVector());

le vecteur (anonyme) sera passé par rvalue reference. La magie C++ 11 est donc délibérément configurée de manière à préserver la valeur réelle des arguments, si possible.

Maintenant, à l'intérieur de perfectSet, vous voulez passer parfaitement l'argument à la surcharge correcte de set(). C'est ici que std::forward Est nécessaire:

template<class T>
void perfectSet(T && t) {
    set(std::forward<T>(t));
}

Sans std :: forward, le compilateur devrait supposer que nous voulons passer t par référence. Pour vous convaincre que cela est vrai, comparez ce code:

void perfectSet(T && t) {
    set(t);
    set(t); // t still unchanged
}

pour ça:

void perfectSet(T && t) {
    set(std::forward<T>(t));
    set(t); // t is now empty
}

Si vous ne transmettez pas explicitement t, le compilateur doit supposer que vous pourriez accéder à nouveau à t et choisir la version de référence lvalue de set. Mais si vous transmettez t, le compilateur en conservera la rvalue-ness et la version de référence rvalue de set() sera appelée. Cette version déplace le contenu de t, ce qui signifie que l'original devient vide.

Cette réponse s'est avérée beaucoup plus longue que ce que j'avais initialement supposé ;-)

149
Bartosz Milewski

Cela fonctionne parce que lorsque le renvoi parfait est appelé, le type T est pas le type valeur, il peut également s'agir d'un type référence.

Par exemple:

template<typename T> void f(T&&);
int main() {
    std::string s;
    f(s); // T is std::string&
    const std::string s2;
    f(s2); // T is a const std::string&
}

En tant que tel, forward peut simplement regarder le type explicite T pour voir ce que vous vraiment l'avez passé. Bien sûr, la mise en œuvre exacte de cette opération est non-rivale, si je me souviens bien, mais c'est là que se trouve l'information.

Lorsque vous vous référez à un nommé rvalue reference, il s'agit bien d'une lvalue. Cependant, forward détecte par les moyens ci-dessus qu'il s'agit en fait d'une valeur rvalue et renvoie correctement une valeur rvalue à transférer.

0
Puppy