Developpez.com - Rubrique C++

Le Club des Développeurs et IT Pro

Comment bien indiquer les arguments en entrée et en sortie d'une fonction ?

Le langage ne dispose pas d'une syntaxe spécifique

Le 2016-07-14 22:46:32, par Pyramidev, Expert éminent
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 :
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 :
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 :
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 :
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 ?
  Discussion forum
24 commentaires
  • jo_link_noir
    Membre expert
    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));.
  • JolyLoic
    Rédacteur/Modérateur
    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 :
    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.
  • Médinoc
    Expert éminent sénior
    Envoyé par oodini
    Mais quel éditeur utilisez-vous donc ?

    Sur le mien, si j'ai un doute en lisant un appel de fonction, il me suffit de passer la souris dessus pour voir sa signature dans un pop-up.
    Et la question est réglée.
    1. L'objectif, c'est de voir du premier coup d'œil, donc "passer la souris dessus" ne remplit pas cet objectif.
    2. Bonne chance pour les codes sur dvp, car à ma connaissance aucun navigateur ne fait ça. Ensuite, si tu copie-colles le code, il te faut un éditeur plus lourd que Notepad++ (qui ne fait pas de reconnaissance) et plus léger que Visual Studio, qui exige un projet avant de le faire.
  • Ehonn
    Membre chevronné
    Envoyé par r0d
    Si c'est juste une question de lisibilité, alors il suffit d'utiliser une convention de nommage. Par exemple iMaVariable, oMaVariable, ioMaVariable.
    Plutôt non, car :
    - comme la solution des macros, le compilateur ne peut pas faire de vérification
    - tu peux utiliser une même variable pour différents paramètres et je ne pense pas que les codes ci dessous soient une bonne idée
    Code :
    1
    2
    3
    T ioT;
    std::cin << ioT; // Devrait être oT;
    std::cout >> ioT; // Devrait être iT;
    Code :
    1
    2
    3
    4
    5
    T t;
    T const & iT = t;
    T & oT = t;
    std::cin << oT;
    std::cout >> iT;
    PS : Je viens de remarquer que IN, OUT, IN/OUT est très bien lors de la déclaration de fonction mais pas lors de l'appel (exemple : std::cin prend la variable en OUT). Du coup, je préfère des mots comme const et mutable, par exemple READ, WRITE, READ/WRITE.
  • Pyramidev
    Expert éminent
    Bonne idée !

    Ça donnerait quelque chose du genre :

    inout.h :
    Code :
    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 :
    void foo(const Type& paramIn, inout<Type> paramInOut);
    Dans le fichier Y :
    Code :
    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).
  • foetus
    Expert éminent sénior
    Et pourquoi pas "un truc à l'ancienne" couplé avec tes recommandations

    Le truc à l'ancienne:
    Code :
    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 :
    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
  • Médinoc
    Expert éminent sénior
    Il n'y a qu'à regarder std::getline() pour voir le problème des paramètres modifiés par la fonction.
    C'est pour ça que je préfère le modèle C#, où c'est explicitement signalé lors de l'appel; malheureusement il serait difficilement possible de causer ça en C++ sans briser la compatibilité. J'aime la proposition du template inout_param par contre, je vais probablement garder ça sous le coude pour mon propre code.
  • oodini
    Membre émérite
    Mais quel éditeur utilisez-vous donc ?

    Sur le mien, si j'ai un doute en lisant un appel de fonction, il me suffit de passer la souris dessus pour voir sa signature dans un pop-up.
    Et la question est réglée.
  • dragonjoker59
    Expert éminent sénior
    La première solution de jo_link_noir n'est pas celle avec les macros, mais celle avec les types intermédiaires, donc là on ferait surtout confiance au compilateur, en fait (comme d'hab, quoi)
  • Ehonn
    Membre chevronné
    Le compilateur devrait optimiser tout ça.