Double Possible:
Avantages de l'utilisation de l'avantJe 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 lathing&& x
vsthing& x
?
Réponses
Trop de publicités?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 ;-)
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.
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.