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?