web-dev-qa-db-fra.com

Avantages de l'utilisation de Forward

En mode de transfert parfait, std::forward est utilisé pour convertir les références rvalue nommées t1 et t2 en références rvalue non nommées. Quel est le but de faire ça? Comment cela affecterait-il la fonction appelée inner si on laisse t1 & t2 comme lvalues?

template <typename T1, typename T2>
void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}
399
Steveng

Vous devez comprendre le problème de transmission. Vous pouvez lisez 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 beaucoup d'essais, mais ils échouent tous à certains égards.


Le plus simple consiste à utiliser une référence 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 celles-ci ne peuvent pas être liées à une référence 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 corrige le problème ci-dessus, mais retourne des flops. Il ne parvient plus à autoriser E à avoir des arguments non constants:

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 références const, mais ensuite const_cast est le const away:

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));
}

Ceci 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 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 2N combinaisons, un cauchemar. Nous aimerions le faire automatiquement.

(C’est effectivement ce que nous demandons au compilateur de faire pour nous en C++ 11.)


En C++ 11, nous avons une chance de résoudre ce problème. ne solution modifie les règles de déduction de modèles sur les types existants, mais cela casse potentiellement beaucoup de code. Nous devons donc trouver un autre moyen.

La solution consiste à utiliser les nouvelles références rvalue-references ; nous pouvons introduire de nouvelles règles lors de la déduction de types rvalue-reference et créer le résultat souhaité. Après tout, nous ne pouvons éventuellement pas casser le code maintenant.

Si une référence à une référence est donnée (la référence à la note 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éation du type" référence lvalue à cv TR "crée le type" référence lvalue à T ", tandis qu'une tentative de création du 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 déduction d’argument modèle: si un argument est une valeur A, nous fournissons l’argument modèle avec une référence lvalue à A. Sinon, nous en déduisons normalement. Cela donne ce qu'on appelle références universelles (le terme ) référence de transfert est maintenant le officiel).

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 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 à faire est de "transmettre" la catégorie de valeur de la variable. Gardez à l'esprit qu'une fois dans la fonction, le paramètre pourrait être passé à 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 fonction deduce et qu'une lvalue nous a été transmise. Cela signifie que T est un A& et que le type de cible pour la conversion statique est A& && ou simplement A&. Puisque x est déjà un A&, nous ne faisons rien et nous nous retrouvons avec une référence lvalue.

Lorsque nous avons passé une valeur rvalue, T correspond à A, le type de cible pour la conversion statique est donc A&&. La conversion produit 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.

La mise en place de ces éléments nous donne une "transmission parfaite":

template <typename A>
void f(A&& a)
{
    E(static_cast<A&&>(a)); 
}

Lorsque f reçoit une lvalue, E obtient une lvalue. Lorsque f reçoit une valeur rvalue, E obtient une valeur rvalue. Parfait.


Et bien sûr, nous voulons nous débarrasser du moche. static_cast<T&&> est cryptique et étrange à 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);
735
GManNickG

Je pense qu'un code conceptuel implémentant std :: forward peut contribuer à la discussion. Ceci est une diapositive de Scott Meyers talk n échantillonneur efficace en C++ 11/14

conceptual code implementing std::forward

La fonction move dans le code est std::move. Il y a une implémentation (de travail) pour cela plus tôt dans cette discussion. J'ai trouvé 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 de l'utilisateur, cela signifie que std::forward est une conversion conditionnelle en une valeur rvalue. Cela peut être utile si j'écris une fonction qui attend une valeur ou une valeur dans un paramètre et souhaite la transmettre à une autre fonction en tant que valeur uniquement si elle a été transmise en tant que valeur. Si je n'ai pas encapsulé le paramètre dans std :: forward, il sera toujours passé comme référence normale.

#include <iostream>
#include <string>
#include <utility>

void overloaded_function(std::string& param) {
  std::cout << "std::string& version" << std::endl;
}
void overloaded_function(std::string&& param) {
  std::cout << "std::string&& version" << std::endl;
}

template<typename T>
void pass_through(T&& param) {
  overloaded_function(std::forward<T>(param));
}

int main() {
  std::string pes;
  pass_through(pes);
  pass_through(std::move(pes));
}

Effectivement, ça imprime

std::string& version
std::string&& version

Le code est basé sur un exemple tiré de l'exposé mentionné précédemment. Diapositive 10, vers 15h00 du début.

53
user7610

En mode de transfert parfait, std :: forward est utilisé pour convertir les références rvalue nommées t1 et t2 en références rvalue non nommées. Quel est le but de faire ça? Comment cela affecterait-il la fonction appelée inner si on laisse t1 & t2 en tant que lvalue?

template <typename T1, typename T2> void outer(T1&& t1, T2&& t2) 
{
    inner(std::forward<T1>(t1), std::forward<T2>(t2));
}

Si vous utilisez une référence rvalue nommée dans une expression, il s'agit en fait d'une lvalue (car vous vous référez à l'objet par son nom). Prenons l'exemple suivant:

void inner(int &,  int &);  // #1
void inner(int &&, int &&); // #2

Maintenant, si nous appelons outer comme ceci

outer(17,29);

nous aimerions que 17 et 29 soient transférés vers # 2 car 17 et 29 sont des littéraux entiers et donc des rvalues. Mais puisque t1 et t2 dans l'expression inner(t1,t2); sont des valeurs, vous appelez # 1 au lieu de # 2. C'est pourquoi nous devons transformer les références en références non nommées avec std::forward. Ainsi, t1 dans outer est toujours une expression lvalue, alors que forward<T1>(t1) peut être une expression rvalue dépendant de T1. Ce dernier n'est qu'une expression de lvalue si T1 est une référence de lvalue. Et T1 ne peut être déduit que d'une référence à une valeur dans le cas où le premier argument de outer était une expression à une valeur.

27
sellibitze

Comment cela affecterait-il la fonction appelée inner si on laisse t1 & t2 en tant que lvalue?

Si, après instanciation, T1 est de type char et que T2 est d'une classe, vous voulez passer t1 par copie et t2 par const référence. Eh bien, à moins que inner() ne les prenne pas par référence non -const, c’est-à-dire, dans ce cas, vous souhaitez le faire également.

Essayez d'écrire un ensemble de fonctions outer() qui l'implémentent sans références rvalue, en déduisant le bon moyen de passer les arguments à partir du type de inner(). Je pense que vous aurez besoin de quelque chose comme 2 ^ 2 d'entre eux, un assez gros gabarit de métadonnées pour en déduire les arguments, et beaucoup de temps pour bien faire les choses, dans tous les cas.

Et puis quelqu'un arrive avec une inner() qui prend des arguments par pointeur. Je pense que cela fait maintenant 3 ^ 2. (Ou 4 ^ 2. Bon Dieu, je ne peux pas être dérangé pour essayer de penser si le pointeur const ferait une différence.)

Et puis imaginez que vous voulez faire cela pour cinq paramètres. Ou sept.

Vous comprenez maintenant pourquoi certains esprits brillants ont proposé une "transmission parfaite": le compilateur fait tout cela pour vous.

11
sbi

Un point qui n’a pas été clarifié, c’est que static_cast<T&&> gère correctement const T&.
Programme:

#include <iostream>

using namespace std;

void g(const int&)
{
    cout << "const int&\n";
}

void g(int&)
{
    cout << "int&\n";
}

void g(int&&)
{
    cout << "int&&\n";
}

template <typename T>
void f(T&& a)
{
    g(static_cast<T&&>(a));
}

int main()
{
    cout << "f(1)\n";
    f(1);
    int a = 2;
    cout << "f(a)\n";
    f(a);
    const int b = 3;
    cout << "f(const b)\n";
    f(b);
    cout << "f(a * b)\n";
    f(a * b);
}

Produit:

f(1)
int&&
f(a)
int&
f(const b)
const int&
f(a * b)
int&&

Notez que 'f' doit être une fonction modèle. S'il est simplement défini comme 'void f (int && a)', cela ne fonctionne pas.

4
lalawawa

Il peut être intéressant de souligner que la transmission directe doit être associée à une méthode externe avec transmission/référence universelle. Utiliser forward par lui-même comme déclaration suivante est autorisé, mais ne sert à rien, sauf à semer la confusion. Le comité standard peut vouloir désactiver cette souplesse sinon pourquoi ne pas utiliser plutôt static_cast?

     std::forward<int>(1);
     std::forward<std::string>("Hello");

À 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 devrions pas nommer une méthode en supposant qu'elle est correctement utilisée, à moins que l'utilisation incorrecte ne soit interdite.

2
colin