178 votes

Comment std::forward travail?

Double Possible:
Avantages de l'utilisation de l'avant

Je sais ce qu'il fait et quand l'utiliser, mais j'ai toujours ne peut pas envelopper la tête autour de la façon dont il fonctionne. Veuillez être aussi précis que possible et d'expliquer lors de l' std::forward serait incorrect s'il a été autorisé à utiliser l'argument de modèle de déduction.

Une partie de ma confusion est ceci: "Si elle a un nom, c'est une lvalue" - si c'est le cas, pourquoi est - std::forward se comportent différemment quand je passe la thing&& x vs thing& x?

281voto

Bartosz Milewski Points 1739

Je pense que l'explication de l' std::forward comme static_cast<T&&> est source de confusion. Notre intuition pour un casting, c'est qu'il convertit un type en un autre type, dans ce cas il s'agirait d'une conversion à une référence rvalue. Il n'est pas! Nous sommes donc expliquer une chose de mystérieux à l'aide d'une autre chose de mystérieux. Cette distribution est définie par un tableau dans Xeo réponse. Mais la question est: Pourquoi? Voici donc ma compréhension:

Supposons que je vous passe un std::vector<T> v que vous êtes censé stocker dans votre structure de données en tant que membre de données _v. Les naïfs (et sécuritaire) solution serait de toujours copier le vecteur dans sa destination finale. Donc, si vous faites cela si un intermédiaire de la fonction (méthode), cette fonction doit être déclarée comme une référence. (Si vous déclarez que la prise d'un vecteur par valeur, vous devez effectuer une supplémentaire totalement inutile copie.)

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

Tout ceci est très bien si vous avez une lvalue dans votre main, mais qu'une rvalue? Supposons que le vecteur est le résultat de l'appel d'une fonction makeAndFillVector(). Si vous avez effectué une affectation directe:

_v = makeAndFillVector();

le compilateur pourrait déplacer le vecteur plutôt que de le copier. Mais si vous introduisez un intermédiaire, set(), les informations sur les rvalue la nature de votre argument serait perdu et une copie peut en être faite.

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

Afin d'éviter cette copie, vous avez besoin de "transfert parfait", ce qui aurait pour résultat optimal code à chaque fois. Si vous avez donné une lvalue, vous voulez que votre fonction à traiter est comme une lvalue et en faire une copie. Si vous avez donné une rvalue, vous voulez que votre fonction de la traiter comme une rvalue et de la déplacer.

Normalement, vous le feriez par la surcharge de la fonction set() séparément pour lvalues et rvalues:

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

Mais imaginez maintenant que vous êtes en train de rédiger un modèle de fonction qui accepte T et des appels set() avec T (ne vous inquiétez pas sur le fait que notre set() n'est définie que pour les vecteurs). Le truc, c'est que vous voulez ce modèle pour appeler la première version de l' set() lorsque la fonction de modèle est instancié avec une lvalue, et la seconde lorsqu'il est initialisé avec une rvalue.

Tout d'abord, quelle devrait être la signature de cette fonction? La réponse est celle-ci:

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

En fonction de comment vous appelez cette fonction de modèle, le type T sera un peu comme par magie déduit différemment. Si vous l'appelez, avec une lvalue:

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

le vecteur v sera passé par référence. Mais si vous l'appelez, avec une rvalue:

perfectSet(makeAndFillVector());

l' (anonyme) vecteur sera passé par référence rvalue. De sorte que le C++11 de la magie est délibérément mis en place de telle manière à préserver la valeur r de la nature des arguments si possible.

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

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

Sans std::forward le compilateur aurait à assumer que nous voulons faire passer t par référence. Pour vous convaincre que c'est vrai, comparez ce code:

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

pour cela:

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

Si vous n'avez pas explicitement en avant t, le compilateur doit défensivement supposons que vous peut accéder t de nouveau et a choisi la lvalue version de référence de l'ensemble. Mais si vous transférez t, le compilateur va préserver la rvalue-ness et la référence rvalue version de set() sera appelée. Cette version se déplace le contenu de t, ce qui signifie que l'originale devient vide.

Cette réponse s'est avéré beaucoup plus long que ce que j'ai d'abord cru ;-)

209voto

Xeo Points 69818

Tout d'abord, jetons un coup d'oeil à ce qu' std::forward t selon la norme:

§20.2.3 [forward] p2

Retourne: static_cast<T&&>(t)

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

Maintenant, rappelez-vous la référence de l'effondrement des règles:

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 puis nous allons prendre un coup d'oeil à une classe qui veut employer le transfert parfait:

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 étape-par-étape de réponse vous aide, vous et les autres à comprendre juste comment std::forward travaux.

0voto

Puppy Points 90818

Cela fonctionne parce que lorsque le transfert parfait est invoquée, le type T est pas le type de la valeur, il peut aussi être un type de 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(s); // T is a const std::string&
}

En tant que tel, forward pouvez simplement regarder l'explicite de type T pour voir ce que vous avez vraiment réussi. Bien sûr, l'exacte mise en œuvre de cette opération est non-trival, si je me souviens bien, mais c'est là où l'information est.

Lorsque vous vous référez à un nommé référence rvalue, alors que c'est en effet une lvalue. Toutefois, forward détecte par les moyens ci-dessus qu'il est en fait une rvalue, et renvoie correctement une rvalue à être transmis.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X