Vous devez comprendre le problème de la redirection. Vous pouvez lire tout le problème en détail mais je vais résumer.
Fondamentalement, étant donné l'expression E(a, b, ... , c)
nous voulons l'expression f(a, b, ... , c)
pour être équivalent. En C++03, c'est impossible. Il existe de nombreuses tentatives, mais elles échouent toutes à certains égards.
Le plus simple est d'utiliser une référence de type lvalue :
template <typename A, typename B, typename C>
void f(A& a, B& b, C& c)
{
E(a, b, c);
}
Mais cela ne permet pas de gérer les valeurs temporaires : f(1, 2, 3);
car ils ne peuvent pas être liés à une référence de type lvalue.
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 pas maintenant E
pour 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 alors const_cast
C'est 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 indé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 tous des 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 les combinaisons, un cauchemar. Nous aimerions que cela se fasse automatiquement.
(C'est effectivement ce que nous demandons au compilateur de faire pour nous en C++11).
Dans C++11, nous avons la possibilité de corriger ce problème. Une solution consiste à modifier les règles de déduction des modèles sur les types existants, mais cela risque de casser une grande partie du code. Nous devons donc trouver un autre moyen.
La solution consiste à utiliser la nouvelle fonction rvalue-références ; nous pouvons introduire de nouvelles règles lors de la déduction des types de référence de valeurs et créer n'importe quel résultat souhaité. Après tout, nous ne pouvons pas casser du code maintenant.
Si l'on donne une référence à une référence (notez que la référence est un terme englobant signifiant à la fois T&
y 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 de modèle : si un argument est une lvalue A, nous fournissons à l'argument de modèle une référence lvalue à A. Sinon, nous déduisons normalement. Cela donne ce qu'on appelle références universelles (le terme référence de transfert est désormais la version officielle).
Pourquoi est-ce utile ? Parce que, combinés, nous conservons la possibilité de garder la trace de la catégorie de valeur d'un type : s'il s'agissait d'une lvalue, nous avons un paramètre de référence lvalue, sinon nous avons un paramètre de référence rvalue.
En 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é comme une lvalue à 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 recevoir le même type de catégorie de valeur que nous ! La solution est la suivante :
static_cast<T&&>(x);
Qu'est-ce que ça fait ? Considérons que nous sommes à l'intérieur du deduce
et on nous a passé une valeur l. Cela signifie que T
est un A&
et donc le type cible pour le cast statique est A& &&
ou simplement A&
. Desde x
est déjà un A&
nous ne faisons rien et nous nous retrouvons avec une référence de type lvalue.
Quand on nous a passé une rvalue, T
es A
donc le type cible pour le cast statique est A&&
. Le cast donne lieu à une expression rvalue, qui ne peut plus être passé à une référence lvalue . Nous avons maintenu la catégorie de valeur du paramètre.
En combinant ces éléments, on obtient un "acheminement parfait" :
template <typename A>
void f(A&& a)
{
E(static_cast<A&&>(a));
}
Quand f
reçoit une lvalue, E
obtient une valeur l. Lorsque f
reçoit une valeur r, E
obtient une valeur r. Parfait.
Et bien sûr, nous voulons nous débarrasser de ce qui est laid. static_cast<T&&>
est cryptique et difficile à mémoriser ; créons 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);