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 :

[Débat] Gestion des paramètres IN/OUT en C++


Sujet :

C++

Vue hybride

Message précédent Message précédent   Message suivant Message suivant
  1. #1
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut [Débat] Gestion des paramètres IN/OUT en C++
    Au niveau de la syntaxe d'appel des fonctions, le C++ ne permet pas à l'utilisateur de différencier du premier coup d'œil les paramètres IN des paramètres IN/OUT.

    Exemple :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Dans un fichier X :
    void foo(const Type& paramIn, Type& paramInOut);
     
    // Dans un autre fichier Y :
    void uneFonction()
    {
        Type obj1, obj2, *ptr1, *ptr2;
     
        // ...
     
        foo(obj1, obj2);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, *ptr2);
        }
    }
    Sans lire le fichier X, le lecteur du fichier Y ne peut pas deviner que le 1er paramètre de foo est IN tandis que le 2e est IN/OUT.

    Une première solution serait d'adopter la convention suivante :
    • Les paramètres IN/OUT sont toujours passés par pointeur vers type non constant.
    • Les paramètres IN sont passés par défaut par référence constante. Ils sont passés par pointeur vers type constant si et seulement si ce pointeur peut être nul.


    Le code devient alors :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Dans un fichier X :
    void bar(const Type& paramIn, Type* paramInOut); // précondition : paramInOut != nullptr
     
    // Dans un autre fichier Y :
    void uneFonction()
    {
        Type obj1, obj2, *ptr1, *ptr2;
     
        // ...
     
        bar(obj1, &obj2);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            bar(*ptr1, ptr2);
        }
    }
    Alors, quand l'utilisateur observe ce code dans le fichier Y, il sait que, selon cette convention, bar ne modifie ni obj1, ni *ptr1. Il n'a pas besoin d'aller chercher cette information dans le fichier X.

    Mais il y a un inconvénient : Dans la version avec foo(*ptr1, *ptr2), grâce à l'étoile, l'utilisateur sait que ptr2 doit être non nul. Par contre, dans la version avec bar(*ptr1, ptr2), l'utilisateur risque d'oublier le test ptr2 != nullptr.
    Pour pallier un peu ce problème, on peut utiliser gsl::not_null (vanté dans cet article), mais ce n'est pas la panacée.

    Une autre solution serait que chaque paramètre IN/OUT soit signalé à chaque fois de manière explicite à l'initiative de l'appelant de la fonction.

    Exemple 1 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Dans le fichier Y :
    void uneFonction()
    {
        Type obj1, obj2, *ptr1, *ptr2;
     
        // ...
     
        foo(obj1, obj2); // peut modifier obj2 !
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, *ptr2); // peut modifier *ptr2 !
        }
    }
    Exemple 2 :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // Dans le fichier Y :
    void uneFonction()
    {
        Type  obj1_mutable;
        Type  obj2_mutable;
        Type* ptr1_canModify;
        Type* ptr2_canModify;
     
        const Type&         obj1 = obj1_mutable;
        const Type&         obj2 = obj2_mutable;
        const Type* const & ptr1 = ptr1_canModify;
        const Type* const & ptr2 = ptr2_canModify;
     
        // ...
     
        foo(obj1, obj2_mutable);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, *ptr2_canModify);
        }
    }
    L'inconvénient est que l'appelant de la fonction a de fortes chances de ne pas avoir ce genre d'initiative.

    Personnellement, actuellement, je passe les paramètres IN/OUT par référence non constante.
    Si je vois un paramètre IN/OUT qui porte à confusion, j'ajoute un commentaire du style "peut modifier tel paramètre" lors de chaque appel à la fonction.
    Et vous ?

  2. #2
    Membre Expert
    Homme Profil pro
    Ingénieur développement logiciels
    Inscrit en
    Juin 2011
    Messages
    760
    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 : 760
    Par défaut
    J'opterais pour un type spécifique avec un constructeur explicite pour que l'appelant n'ait l'alternative de l'ignorer.

    Plus une fonction de construction pour ne pas se taper la déduction du type: foo(obj1, inout_param(obj2));.

  3. #3
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Bonne idée !

    Ça donnerait quelque chose du genre :

    inout.h :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    #include <type_traits>
     
    template<typename T>
    class inout {
        static_assert(std::is_same< T, typename std::remove_const<T>::type >::value, "Erreur : Le type doit ne pas être constant.");
    private:
        T& m_ref;
    public:
        explicit inout(T& x) : m_ref(x) {}
        T& get() {return m_ref;}
    };
     
    template<typename T>
    inout<T> make_inout(T& x) { // sera inutile en C++17
        return inout<T>(x);
    }
     
    template<typename T>
    class opt_inout {
        static_assert(std::is_same< T, typename std::remove_const<T>::type >::value, "Erreur : Le type doit ne pas être constant.");
    private:
        T* const m_ptr;
    public:
        explicit opt_inout(T* x) : m_ptr(x) {}
        T* get() {return m_ptr;}
    };
     
    template<typename T>
    inout<T> make_opt_inout(T* x) { // sera inutile en C++17
        return opt_inout<T>(x);
    }
    Dans le fichier X :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    void foo(const Type& paramIn, inout<Type> paramInOut);
    Dans le fichier Y :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void uneFonction()
    {
        Type obj1, obj2, *ptr1, *ptr2;
     
        // ...
     
        foo(obj1, make_inout(obj2));
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, make_inout(*ptr2));
        }
    }
    EDIT : ajout de opt_inout dans inout.h pour prendre en compte le cas des paramètres IN/OUT optionnels (pointeurs vers type non constant qui peuvent être nuls).

  4. #4
    Rédacteur/Modérateur
    Avatar de JolyLoic
    Homme Profil pro
    Développeur informatique
    Inscrit en
    Août 2004
    Messages
    5 463
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 50
    Localisation : France, Yvelines (Île de France)

    Informations professionnelles :
    Activité : Développeur informatique
    Secteur : High Tech - Éditeur de logiciels

    Informations forums :
    Inscription : Août 2004
    Messages : 5 463
    Par défaut
    Certains coding styles (ceux de google par exemple) imposent ta solution à base de pointeur pour in/out, référence pour in.

    J'avoue avoir du mal à être convaincu par cet argument, pour les raisons suivantes :
    - Le cas le plus courant de paramètre in/out est géré par la syntaxe objet : f(a /*in/out*/, b); => a.f(b); (petit aparté amusant : parmi les personnes que j'ai pu rencontrer qui disent qu'il faudrait distinguer au niveau de l'appel les passages par références constante ou non, je n'ai jamais rencontré personne voulant différencier au niveau de l'appel une fonction membre const d'une non const, alors que c'est exactement le même problème).
    - En dehors de ce cas, les paramètres in/out sont généralement assez rares, et les paramètres n'étant que out sont généralement avec profit transmis par la valeur de retour de la fonction.
    - Les exemples comme tu as montré avec des fonction foo et bar peuvent sembler convaincants, mais généralement, dans du vrai code, le nom de la fonction et éventuellement de ses arguments permet de lever le doute sans aucun effort. Je ne suis encore jamais tombé sur une exemple où je ressentre une nécessité d'expliciter la modification du paramètre (sauf dans du vieux code où une référence non constante est utilisée pour un paramètre out)...

    Si je prends un exemple, le premier qui me vient (il n'y a pas tellement de passages par référence non constante dans la lib standard...) :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    std::list<int> l1, l2;
    /*...*/
    auto it = l1.find(/* ...*/);
    l1.splice(it, l2);
    Le gars qui n'a pas compris que l2 va être modifié par cet appel n'a clairement pas compris ce que fait la fonction splice... Et si tel est le cas, je ne pense pas qu'ajouter quelque décoration que ce soit au site d'appel va l'aider.
    Ma session aux Microsoft TechDays 2013 : Développer en natif avec C++11.
    Celle des Microsoft TechDays 2014 : Bonnes pratiques pour apprivoiser le C++11 avec Visual C++
    Et celle des Microsoft TechDays 2015 : Visual C++ 2015 : voyage à la découverte d'un nouveau monde
    Je donne des formations au C++ en entreprise, n'hésitez pas à me contacter.

  5. #5
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    Citation Envoyé par JolyLoic Voir le message
    petit aparté amusant : parmi les personnes que j'ai pu rencontrer qui disent qu'il faudrait distinguer au niveau de l'appel les passages par références constante ou non, je n'ai jamais rencontré personne voulant différencier au niveau de l'appel une fonction membre const d'une non const, alors que c'est exactement le même problème
    J'y avais pensé, mais le besoin est moins fort dans le cas du paramètre this des fonctions membres d'une classe.
    Le paramètre a est souvent IN/OUT dans un appel de la forme a.f(b) mais rarement dans un appel de la forme f(a, b).
    Du coup, quand on analyse de manière rapide et superficielle le code pour chercher où la variable a est modifiée, on risque d'ignorer les passages comme f(a, b) si le nom de la fonction ne sous-entend pas que a est IN/OUT.

    Citation Envoyé par JolyLoic Voir le message
    - Les exemples comme tu as montré avec des fonction foo et bar peuvent sembler convaincants, mais généralement, dans du vrai code, le nom de la fonction et éventuellement de ses arguments permet de lever le doute sans aucun effort. Je ne suis encore jamais tombé sur une exemple où je ressentre une nécessité d'expliciter la modification du paramètre (sauf dans du vieux code où une référence non constante est utilisée pour un paramètre out)...
    L'exemple que je vais donner est très général mais, dans un programme d'une interface graphique, que penses-tu du cas d'un constructeur d'une classe qui représente une fenêtre ?
    Les éventuelles infos que la fenêtre peut modifier sont dans des paramètres IN/OUT. Les autres paramètres sont IN.

  6. #6
    Expert confirmé
    Homme Profil pro
    Analyste/ Programmeur
    Inscrit en
    Juillet 2013
    Messages
    4 763
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Bouches du Rhône (Provence Alpes Côte d'Azur)

    Informations professionnelles :
    Activité : Analyste/ Programmeur

    Informations forums :
    Inscription : Juillet 2013
    Messages : 4 763
    Par défaut
    Et pourquoi pas "un truc à l'ancienne" couplé avec tes recommandations

    Le truc à l'ancienne:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    /** 
     * This is a definition which has sole purpose of helping readability.
     * It indicates that formal parameter is an input parameter.
     */
    #ifndef IN
    #define IN
    #endif
     
    /** 
     * This is a definition which has sole purpose of helping readability.
     * It indicates that formal parameter is an output parameter.
     */
    #ifndef OUT
    #define OUT
    #endif
     
    /** 
     * This is a definition which has sole purpose of helping readability.
     * It indicates that formal parameter is both input and output parameter.
     */
    #ifndef INOUT
    #define INOUT
    #endif
     
    /** 
     * This is a definition which has sole purpose of helping readability.
     * It indicates that formal parameter is an optional parameter.  
     */
    #ifndef OPTIONAL
    #define OPTIONAL
    #endif

    Et à l'utilisation:
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    void bar(
            IN const Type& param_01,
            INOUT Type* param_02);

    Après on peut étendre avec des INOUT_NOT_NULL par exemple et tester si l'IDE les garde lorsqu'il affiche dans son info-bulle le prototype de la fonction

  7. #7
    Membre Expert
    Profil pro
    Inscrit en
    Mars 2007
    Messages
    1 415
    Détails du profil
    Informations personnelles :
    Localisation : France, Paris (Île de France)

    Informations forums :
    Inscription : Mars 2007
    Messages : 1 415
    Par défaut
    Hello

    Ce n'est pas vraiment un débat, mais plutôt une question de préférences en terme de conventions. L'argument principal de Google est : le code doit être optimisé pour la lecture, pas pour l'écriture. Pour ma part, comme Loïc, l'argument me plait mais je ne suis pas convaincu par une application aussi extrême. Du reste, les paramètres IN/OUT sont en effet assez rare dans du code bien conçu. Les paramètres OUT vont le devenir encore plus avec les structured bindings que C++17 va introduire. Et, enfin l'absence de const et de pointeurs dans d'autres langages rend l'usage de IN/OUT déconseillé et leur distinction impossible "par pointeur" dans ces langages. Ce n'est pas nécessairement une bonne idée, du coup, de le faire en C++, car ça rajoute encore un coût d'apprentissage.

    Pour ma part, je n'ai jamais de IN/OUT dans mon code alors ça ne me pose pas trop de problèmes. Quant aux macros proposés par foetus, je ne vois pas trop l'utilité, c'est un peu de l'over engineering à mon goût.

    Edit: correction suggérée par Pyramidev appliquée.

  8. #8
    Membre Expert
    Avatar de Pyramidev
    Homme Profil pro
    Tech Lead
    Inscrit en
    Avril 2016
    Messages
    1 513
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Localisation : France, Haute Garonne (Midi Pyrénées)

    Informations professionnelles :
    Activité : Tech Lead

    Informations forums :
    Inscription : Avril 2016
    Messages : 1 513
    Par défaut
    @foetus : Ta macro INOUT est une bonne alternative au commentaire :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
        foo(obj1, INOUT obj2);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, INOUT *ptr2);
        }
    A un moment, j'avais pensé à ceci :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
        foo(obj1, /* INOUT */ obj2);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, /* INOUT */ *ptr2);
        }
    Mais c'était moins bien, car c'était plus difficile de masquer des portions de code :
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    /*
        foo(obj1, /* INOUT */ obj2);
        if(ptr1 != nullptr && ptr2 != nullptr) {
            foo(*ptr1, /* INOUT */ *ptr2);
        }
    */
    Cependant, je préfère la solution de jo_link_noir.

    Ta macro IN serait trop présente dans le code à mon goût.

    A la place de ta macro OUT, je préfère mettre le paramètre dans le retour de la fonction, même si cela implique de retourner une structure. L'avantage du retour de la fonction, c'est qu'il n'y a pas d'ambigüité possible : c'est forcément OUT.

    Je ne vois pas l'intérêt de la macro OPTIONAL en C++.
    En langage C, ça me semble bien car on est inondé de pointeurs. Quand la majorité des pointeurs doivent ne pas être nuls, il est pertinent de notifier ceux qui peuvent l'être.
    Par contre, en C++, quand un pointeur doit ne pas être nul, on peut souvent le remplacer par une référence. Dans les quelques cas restants, on peut utiliser std::reference_wrapper (référence réassignable) ou, éventuellement, gsl::not_null.

    Citation Envoyé par jblecanard Voir le message
    Du reste, les paramètres IN/OUT sont en effet assez rare dans du code bien conçu. Ils vont le devenir encore plus avec les structured bindings que C++17 va introduire.
    Je vois un rapport entre les structured bindings du C++17 et les paramètres OUT, mais quel est le rapport avec les paramètres IN/OUT ?

    Citation Envoyé par Ehonn Voir le message
    Si les règles définissent que l'utilisation d'un pointeur apporte la sémantique de in/out.
    Comment se caractérisent les autres sémantiques ?
    Par exemples, pour le déplacement, le partage et la responsabilité mémoire.
    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    //////////////
    // Déplacement
     
    // Déclaration
    void f(Type&& param);
    void g(std::unique_ptr<      Type> param);
    void h(std::unique_ptr<const Type> param);
     
    // Appels
    Type                  x(blabla);
    std::unique_ptr<Type> y(new TypeOuClasseQuiDeriveDeType(blabla));
    std::unique_ptr<Type> z(new TypeOuClasseQuiDeriveDeType(blabla));
    f(std::move(x));
    g(std::move(y));
    h(std::move(z));
     
    // Appels avec un objet temporaire :
    f(Type(blabla));
    g(std::make_unique<TypeOuClasseQuiDeriveDeType>(blabla));
    h(std::make_unique<TypeOuClasseQuiDeriveDeType>(blabla));
     
    ////////////////////////////////////////////////////
    // Partage de la responabilité de détruire la donnée
     
    // Déclaration
    void foo(std::shared_ptr<      Type> param);
    void bar(std::shared_ptr<const Type> param);
     
    // Appels
    auto x_shared = std::make_shared<TypeOuClasseQuiDeriveDeType>(blabla);
    f2(x_shared);
    g2(x_shared);

  9. #9
    Membre expérimenté Avatar de RPGamer
    Homme Profil pro
    Ingénieur en systèmes embarqués
    Inscrit en
    Mars 2010
    Messages
    168
    Détails du profil
    Informations personnelles :
    Sexe : Homme
    Âge : 35
    Localisation : Suisse

    Informations professionnelles :
    Activité : Ingénieur en systèmes embarqués

    Informations forums :
    Inscription : Mars 2010
    Messages : 168
    Par défaut
    Quid de la sémantique de la fonction ?

    Code : Sélectionner tout - Visualiser dans une fenêtre à part
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    void setCoordinates(const Coord &coordinates);
     
    void calculateCoordinates(Coord &coordinates);
    // ou
    Coord calculateCoordinates();
     
    void moveRight(Coord &coordinates);
    // ou
    Coord toRight(const &Coord coordinates);
    // ou
    coordinates += {1, 0};

Discussions similaires

  1. Gestion des paramètres dans le temps
    Par Terminator dans le forum SQL
    Réponses: 8
    Dernier message: 06/05/2009, 14h34
  2. Gestion des paramètres
    Par delavega dans le forum ActionScript 1 & ActionScript 2
    Réponses: 0
    Dernier message: 21/04/2009, 21h03
  3. [bash / sh] Mauvaise gestion des paramètres d'entrée
    Par Rei Angelus dans le forum Shell et commandes GNU
    Réponses: 7
    Dernier message: 05/04/2008, 06h08
  4. [MFC][VC6.0] Gestion des paramètres régionaux
    Par Yellowmat dans le forum MFC
    Réponses: 3
    Dernier message: 11/01/2008, 11h04
  5. [Conseils généraux] Gestion des paramètres
    Par Julien Dufour dans le forum Access
    Réponses: 1
    Dernier message: 02/05/2006, 11h04

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