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 E
d'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_cast
le const
loin:
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 deduce
fonction, et nous avons reçu une valeur l. Cela signifie que T
est un A&
, et donc le type cible pour la distribution statique est A& &&
, ou tout simplement A&
. Puisque x
c'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, T
is 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 f
reçoit une lvalue, E
obtient une lvalue. Quand f
reçoit une rvalue, E
obtient 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);
f
une fonction et non une expression?const int i
sera accepté:A
est déduit deconst int
. Les échecs concernent les littéraux rvalues. Notez également que pour l'appel àdeduced(1)
, x estint&&
nonint
(le transfert parfait ne fait jamais de copie, comme ce serait le cas s'ilx
s'agissait d'un paramètre par valeur). Le simple faitT
estint
. La raison quix
évalue une valeur l dans le redirecteur est que les références rvalue nommées deviennent des expressions lvalue.forward
oumove
ici? Ou est-ce juste une différence sémantique?std::move
doit être appelé sans arguments de modèle explicites et se traduit toujours par une valeur r, tandis questd::forward
peut finir comme l'un ou l'autre. À utiliserstd::move
lorsque vous savez que vous n'avez plus besoin de la valeur et que vous souhaitez la déplacer ailleurs, utilisezstd::forward
pour cela en fonction des valeurs transmises à votre modèle de fonction.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
La fonction
move
dans le code eststd::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::forward
d'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.Effectivement, il imprime
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.
la source
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:
Maintenant, si on appelle
outer
comme çanous 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
t1
ett2
dans l'expressioninner(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 avecstd::forward
. Donc,t1
inouter
est toujours une expression lvalue tandis queforward<T1>(t1)
peut être une expression rvalue selonT1
. Cette dernière n'est qu'une expression lvalue siT1
est une référence lvalue. EtT1
n'est déduit que pour être une référence lvalue dans le cas où le premier argument à external était une expression lvalue.la source
Si, après instanciation,
T1
est de typechar
etT2
appartient à une classe, vous souhaitez passert1
par copie ett2
parconst
référence. Eh bien, à moins de lesinner()
prendre par non-const
ré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 duinner()
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 leconst
pointeur 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.
la source
Un point qui n'a pas été rendu clair est que cela se
static_cast<T&&>
gèreconst T&
correctement aussi.Programme:
Produit:
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.
la source
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?
À 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.
la source