592 votes

Quels sont les principaux objectifs de l'utilisation de std::forward et les problèmes qu'il résout ?

Dans un acheminement parfait, std::forward est utilisé pour convertir les références rvalue nommées t1 y t2 à des références rvalue non nommées. Quel est l'intérêt de faire cela ? Comment cela affecterait-il la fonction appelée inner si nous partons t1 & t2 en tant que valeurs l ?

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

1061voto

GManNickG Points 155079

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

1 votes

N'est-ce pas ? f être une fonction, et non une expression ?

0 votes

@sbi : Haha. :) <3 @mfukar : C'est une expression d'appel de fonction. f lui-même peut être tout ce qui est "appelable" ; une fonction, un pointeur de fonction ou un objet de fonction.

0 votes

Merci pour votre réponse complète. Juste une question, dans deduce(1), x est de type int&& ou int ? Parce que je pense avoir lu quelque part que si c'est un type rvalue, le T serait résolu en int. Donc avec le && supplémentaire, il deviendrait de type int&&. Quelqu'un pourrait-il clarifier ce point ? thbecker.net/articles/rvalue_references/section_08.html

118voto

user7610 Points 820

Je pense que le fait d'avoir un code conceptuel implémentant std::forward peut aider à la compréhension. C'est une diapositive de la conférence de Scott Meyers Un échantillonneur C++11/14 efficace

conceptual code implementing std::forward

Fonction move dans le code est std::move . Il y a une implémentation (fonctionnelle) pour cela plus tôt dans cette conférence. 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 un cast conditionnel vers une rvalue. Cela peut être utile si j'écris une fonction qui attend soit une lvalue soit une rvalue dans un paramètre et que je veux le passer à une autre fonction en tant que rvalue seulement s'il a été passé en tant que rvalue. Si je n'avais pas enveloppé le paramètre dans std::forward, il serait toujours transmis comme une 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));
}

Bien sûr, il imprime

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

Le code est basé sur un exemple tiré de la conférence mentionnée précédemment. Slide 10, à environ 15:00 du début.

3 votes

Votre deuxième lien a fini par pointer à un endroit complètement différent.

3 votes

Wow, grande explication. J'ai commencé à partir de cette vidéo : youtube.com/watch?v=srdwFMZY3Hg mais après avoir lu votre réponse, je le sens enfin :)

50voto

sellibitze Points 13607

Dans le transfert parfait, std::forward est utilisé pour convertir la référence rvalue nommée t1 et t2 en référence rvalue non nommée. Quel est le but de faire cela ? Comment cela affecterait-il la fonction appelée inner si nous laissons t1 & t2 comme 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 faites référence à 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 voudrions que 17 et 29 soient transférés vers #2 parce que 17 et 29 sont des littéraux entiers et en tant que tels des rvalues. Mais comme t1 y t2 dans l'expression inner(t1,t2); sont des valeurs l, vous invoqueriez le numéro 1 au lieu du numéro 2. C'est pourquoi nous avons besoin de transformer les références en références non nommées avec std::forward . Donc, t1 en outer est toujours une expression de type lvalue alors que forward<T1>(t1) peut être une expression rvalue en fonction de T1 . Cette dernière n'est une expression lvalue que si T1 est une référence à une lvalue. Et T1 est seulement déduit comme étant une référence lvalue dans le cas où le premier argument de outer était une expression lvalue.

3 votes

Il s'agit d'une explication un peu édulcorée, mais très bien faite et fonctionnelle. Les gens devraient d'abord lire cette réponse, puis aller plus loin s'ils le souhaitent.

0 votes

@sellibitze Encore une question, quelle affirmation est la bonne quand on déduit int a;f(a) : "puisque a est une valeur l, alors int(T&&) équivaut à int(int& &&)" ou "pour que T&& soit égal à int&, alors T devrait être int&" ? Je préfère la dernière solution.

13voto

sbi Points 100828

Comment cela affecterait-il la fonction appelée inner si nous laissons t1 & t2 comme valeurs lvalues ?

Si, après avoir instancié, T1 est de type char y T2 est d'une classe, vous voulez passer t1 par copie et t2 par const référence. Eh bien, à moins que inner() les prend par non const c'est-à-dire, dans ce cas, vous voulez aussi le faire.

Essayez d'écrire un ensemble de outer() qui l'implémentent sans références rvalue, en déduisant la bonne façon de passer les arguments de la fonction inner() Le type de l'entreprise. Je pense que vous aurez besoin de quelque chose comme 2^2 d'entre eux, d'une quantité assez importante de méta-modèles pour déduire les arguments, et de beaucoup de temps pour que cela soit correct dans tous les cas.

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

Et puis imaginez que vous voulez faire ça pour un paramètre de cinq. Ou sept.

Vous savez maintenant pourquoi certains esprits brillants ont inventé la "redirection parfaite" : Le compilateur fait tout ça pour vous.

8voto

Bill Chapman Points 61

Un point qui n'a pas été clairement établi est que static_cast<T&&> poignées const T& correctement aussi.
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. Si elle est simplement définie comme 'void f(int&& a)', cela ne fonctionne pas.

0 votes

Bon point, donc T&& dans une distribution statique suit également les règles d'effondrement de la référence, n'est-ce pas ?

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X