IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
Navigation

Inscrivez-vous gratuitement
pour pouvoir participer, suivre les réponses en temps réel, voter pour les messages, poser vos propres questions et recevoir la newsletter

C++ Discussion :

Utiliser une fonction ayant pour paramètre une référence de rvalue


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre averti
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Par défaut Utiliser une fonction ayant pour paramètre une référence de rvalue
    Bonjour,

    J'ai besoin d'utiliser la fonction value_or (librairie tomlplusplus). Pour simplifier, disons que j'ai besoin de l'appeler sur un bool.
    Voici la fonction que je veux écrire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    auto toml::node::value_or(T&& default_value) const;   // Signature de value_or 
     
    // Ma fonction
    bool lireAvecValeurParDefaut(toml::table& t, const string& nomVal, bool defaut) {
        // Lit une valeur dans un table et la retourne, si la valeur n'existe pas elle retourne une valeur par défaut
        return t[nomVal].value_or<bool>(defaut);
    }
    Problème, cette fonction ne marche pas vu que dans value_or, default_value doit être une référence de rvalue.
    Je ne connaissais pas bien cette notion mais de ce que j'en ai compris les type T&& permettent de s'assurer que la ressource est temporaire et que la mémoire utilisée peut être récupérée par la fonction (n'hésitez pas à me corriger).

    J'ai deux versions alternatives de ma fonction qui marchent, mais qui sont plus ou moins satisfaisantes :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    bool lireAvecValeurParDefaut(toml::table& t, const string& nomVal, bool defaut) {
        return t[nomVal].value_or<bool>(bool(defaut));       // Version 2
        return t[nomVal].value_or<bool>(std::move(defaut));  // Version 3
    }
    Je préfère la version 3 parce que (sauf erreur de ma part) la version 2 fait une copie supplémentaire. Néanmoins, la version 3 mais ça m'a l'air "inutilement compliquée" et moins lisible que ma version initiale.

    Par exemple, j'avais pensé à faire passer "defaut" comme une référence de rvalue :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    bool lireAvecValeurParDefaut(toml::table& t, const string& nomVal, bool&& defaut)
    Mais là non plus ça ne fonctionne pas, vu qu'il estime que defaut peut être utilisée ailleurs dans ma fonction (si j'ai bien compris).

    Donc voici ma question, est-il possible de faire autrement que ces deux solutions qui marchent ? Est ce qu'il y a quelque chose de fondamental qui m'échappe ?

    Merci d'avance

  2. #2
    Invité de passage

    Profil pro
    Inscrit en
    Décembre 2013
    Messages
    397
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2013
    Messages : 397
    Par défaut
    Citation Envoyé par iXeRay Voir le message
    doit être une référence de rvalue
    C'est un peu un piege, il s'agit en fait d'une universal/forwarding reference https://quuxplusone.github.io/blog/2...hat-they-need/ parce que c'est un template et pas un "vrai" type.

    La difference est importante, puisqu'une universal/forwarding reference accepte aussi bien une lvalue qu'une rvalue, alors qu'une rvalue reference accepte uniquement une rvalue. (Donc ton code devrait a priori fonctionner)

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void foo(bool&&) {} // rvalue ref
    void bar(auto&&) {} // forwarding ref
     
    int main() {
        bool b; // lvalue
        // foo(b); // erreur avec une lvalue
        foo(true); // ok avec une rvalue
        bar(b); // ok avec une lvalue
        bar(true); // ok avec une rvalue
    }
    Citation Envoyé par iXeRay Voir le message
    Est ce qu'il y a quelque chose de fondamental qui m'échappe ?
    Oui Sans entrer dans les details, il faut faire la distinction entre le type et la categorie de valeur https://en.cppreference.com/w/cpp/la...value_category. Dans le code suivant, par exemple :

    La variable/parametre "default" est une lvalue (categorie de valeur) et une rvalue reference (type).

    Citation Envoyé par iXeRay Voir le message
    Donc voici ma question, est-il possible de faire autrement que ces deux solutions qui marchent ?
    Si tu utilise dans ta fonction "bool&&" ou "bool", alors ce n'est plus une universal/forwarding reference et ta fonction n'acceptera que une lvalue ou une rvalue. Il faut donc que tu decides pour commencer comment ta fonction peut etre appellee.

    Si tu veux garder le cote "universel", alors tu ne peux pas utiliser "bool&&". Il faut utiliser "bool", "const bool&" ou un template aussi "T&&" ou "auto&&". Et alors utiliser soit std::move, soit std::forward.

    A lire, par exemple http://thbecker.net/articles/rvalue_...ection_08.html sur le perfect forwarding.

  3. #3
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    750
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Hérault (Languedoc Roussillon)

    Informations professionnelles :
    Activité : Ingénieur développement logiciels

    Informations forums :
    Inscription : Juin 2011
    Messages : 750
    Par défaut
    En forçant le type du template à bool, tu forces la fonction à prendre (bool&&) qui est une rvalue (donc plus une forwarding reference). Dans cette logique, pour avoir une lvalue, il faut faire .value_or<bool&>(defaut). Mais comme spécifier le type est redondant et propice aux erreurs, autant ne jamais l'indiquer:

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    return t[nomVal].value_or(defaut);
    Comme l'indique mintho carmo, tu peux aussi ajouter std::move / std::forward selon le besoin, mais pour un type scalaire tel que bool, c'est clairement inutile: la copie est gratuite.

  4. #4
    Invité de passage

    Profil pro
    Inscrit en
    Décembre 2013
    Messages
    397
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2013
    Messages : 397
    Par défaut
    Citation Envoyé par jo_link_noir Voir le message
    la copie est gratuite
    Merci, j'ai oublié de préciser que c'etait osef pour un bool. Je partais du princique que bool etait pour l'exemple et qu'il pouvait utiliser d'autres types.

  5. #5
    Membre averti
    Homme Profil pro
    Étudiant
    Inscrit en
    Octobre 2018
    Messages
    28
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 27
    Localisation : France, Isère (Rhône Alpes)

    Informations professionnelles :
    Activité : Étudiant

    Informations forums :
    Inscription : Octobre 2018
    Messages : 28
    Par défaut
    Merci pour les réponses détaillées. J'ai mis longtemps à répondre parce qu'il m'a fallu un peu de temps pour faire des tests et comprendre (ou pas) ces nouveaux concepts.

    Je ne savais pas que ma simplification en passant à un bool pouvait changer le problème donc voici la version template que j'essayais d'écrire et qui ne fonctionnait pas.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    T lireAvecValeurParDefaut(toml::table& t, const string& nomVal, T defaut) {
        // Lit une valeur dans un table et la retourne, si la valeur n'existe pas elle retourne une valeur par défaut
        return t[nomVal].value_or<T>(defaut);
    }
     
    bool b = lireAvecValeurParDefaut<bool>(t, "nomVal", false);
    Si maintenant je n'impose plus le type comme jo_link_noir, ça fonctionne :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    return t[nomVal].value_or(defaut);
    Pourquoi imposer un type template pose aussi problème ? Je pensais qu'en ne spécifiant pas le type c'était automatiquement déduit par le compilateur et que donc c'était équivalent.
    Je pensais que spécifier le type était facultatif mais que ça ne pouvait pas poser de problème (à moins de se tromper de type) mais ici ça semble être le cas.

    Mais comme spécifier le type est redondant et propice aux erreurs, autant ne jamais l'indiquer
    Je pensais que préciser le type était mieux (plus clair, évite certaines erreurs en explicitant nos intentions clairement). Pourquoi est ce que c'est une mauvaise pratique finalement ?

    Pour revenir à mon problème, j'ai aussi essayé de faire varier d'autres aspects de mon code :
    - imposer ou non le type à lireAvecValeurParDefaut
    - changer le type de defaut pour un T&& ou un const T&
    Mais ça n'a aucun impact, seul ne pas imposer le type de value_or semble fonctionner.

    Du coup je suis pas trop sûr d'avoir compris (je précise que même si je pose de nouvelles questions pour mieux comprendre, la solution fournie me va très bien et c'est ce que je vais utiliser).

    Au début, value_or accepte une universal reference, OK.
    Si j'impose le type, même si c'est un template (même si j'ai pas trop compris pourquoi), je le transforme en rvalue reference, OK. Est ce possible d'imposer un type sans que ça ait un impact ? J'ai essayé value_or<T&&> sans succès.
    Si il n'accepte qu'une rvalue reference, une fois dans ma fonction il faut que je transforme "defaut" pour que ça soit compatible :
    - move rend disponible la mémoire de "defaut" (value_or peut l'utiliser)
    - forward dit juste à la fonction de traiter "defaut" comme il a été passé à lireAvecValeurParDefaut
    - T(defaut) fait une copie
    Dans lireAvecValeurParDefaut je ne peux avoir "defaut" qui soit une rvalue, peut importe le type que j'utilise en entrée, c'est ça ?

    Merci

  6. #6
    Invité de passage

    Profil pro
    Inscrit en
    Décembre 2013
    Messages
    397
    Détails du profil
    Informations personnelles :
    Localisation : France

    Informations forums :
    Inscription : Décembre 2013
    Messages : 397
    Par défaut
    Citation Envoyé par iXeRay Voir le message
    Pourquoi imposer un type template pose aussi problème ? ... (à moins de se tromper de type)
    Tu donnes toi meme l'explication a ton probleme.

    La deduction des types en C++ suis des regles parfois tres complexes. Je te conseille de lire en detail les liens que je t'ai donne et faire des recherches sur ces concepts. Je te conseille aussi le livre "Effective Modern C++", de Scott Meyers, il y a un chapitre sur la deduction de type dans les templates.

    Pour simplifier, cf ce code qui reproduit ton probleme :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    template<typename T>
    void foo(T&&) {
    }
     
    template<typename T>
    void bar(T x) {
        foo<T>(x);
    }
     
    int main() {
        bar(true);
    }
    Et le message d'erreur :

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    prog.cc: In instantiation of 'void bar(T) [with T = bool]':
    prog.cc:12:8:   required from here
    prog.cc:8:12: error: cannot bind rvalue reference of type 'bool&&' to lvalue of type 'bool'
        8 |     foo<T>(x);
          |            ^
    prog.cc:2:10: note:   initializing argument 1 of 'void foo(T&&) [with T = bool]'
        2 | void foo(T&&) {
          |          ^~~
    Comme tu le vois, le type de "x" est "bool&&" alors que le type de la fonction est "bool".

    Sans typage explicite, le type est correctement deduit en "bool&&".

    Par contre, tu aurais du voir ce probleme grace aux messages d'erreur. As tu correctement active les warnings ?

    Citation Envoyé par iXeRay Voir le message
    Je pensais que préciser le type était mieux (plus clair, évite certaines erreurs en explicitant nos intentions clairement).
    Parce que, comme tu l'as vu, ca peut etre complexe de deviner exactement le type.

    Et encore, tu es tombe dans le "bon" scenario : le probleme provoque une erreur de compilation. Le pire cas, c'est quand le compilateur accepte le probleme, en faisant des conversions implicites, faire des copies et baisser fortement les performances.

    Citation Envoyé par iXeRay Voir le message
    Pourquoi est ce que c'est une mauvaise pratique finalement ?
    La pratique est de ne pas expliciter le type, quand c'est pas necessaire. Aussi bien pour les fonctions que pour les classes.

    Il y a eu un large debat sur le sujet de la deduction de types vs explicite, lors des debats sur AAA (Almost Always Auto). Tu peux faire des recherches aussi sur le sujet.

    Citation Envoyé par iXeRay Voir le message
    Est ce possible d'imposer un type sans que ça ait un impact ?
    Non, tu perds dans tous les cas l'aspect "universel". Sans universal/forwadring references, il faut une surcharge de fonctions.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    template<typename T>
    void bar(T&&); // universal ref
     
    // devient :
    void bar(bool&&); // rvalue ref
    void bar(bool&); // lvalue ref
    Citation Envoyé par iXeRay Voir le message
    Si il n'accepte qu'une rvalue reference, une fois dans ma fonction il faut que je transforme "defaut" pour que ça soit compatible :
    - move rend disponible la mémoire de "defaut" (value_or peut l'utiliser)
    - forward dit juste à la fonction de traiter "defaut" comme il a été passé à lireAvecValeurParDefaut
    - T(defaut) fait une copie
    - forward ne sert a rien si c'est pas une universal/forwarding reference.
    - std::move` permet de demander au compilateur de "deplacer" la valeur". Dans le cas d'un type fondamental comme bool, le deplacement sera une copie de toute facon.

    Par contre, si c'est pas un type fondamental (par exemple string, vector, etc), alors la copie sera couteuse. Surtout que tu feras 2 copies (une a l'appel de ta fonction et une a l'appel de value_or).

    Le code correct est d'utiliser le perfect forwarding. C'est fait exactement pour cette situation.

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <utility>
     
    template<typename T>
    void foo(T&&) {
    }
     
    template<typename T>
    void bar(T&& x) {
        foo(std::forward<T>(x));
    }
     
    int main() {
        bar(true);
    }

+ Répondre à la discussion
Cette discussion est résolue.

Discussions similaires

  1. Réponses: 0
    Dernier message: 05/04/2018, 15h20
  2. Passer une fonction ayant des paramètres en paramètre
    Par doudou_rennes dans le forum C#
    Réponses: 2
    Dernier message: 31/08/2010, 13h12
  3. Réponses: 3
    Dernier message: 04/02/2010, 11h03
  4. [VBA-Excel] appelle fonction ayant plusieurs paramètres
    Par thierryyyyyyy dans le forum Macros et VBA Excel
    Réponses: 10
    Dernier message: 26/02/2007, 17h01
  5. Utiliser une référence dans une classe
    Par Davidbrcz dans le forum C++
    Réponses: 4
    Dernier message: 07/01/2007, 20h13

Partager

Partager
  • Envoyer la discussion sur Viadeo
  • Envoyer la discussion sur Twitter
  • Envoyer la discussion sur Google
  • Envoyer la discussion sur Facebook
  • Envoyer la discussion sur Digg
  • Envoyer la discussion sur Delicious
  • Envoyer la discussion sur MySpace
  • Envoyer la discussion sur Yahoo