[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:
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 ?
Je suis pas expert mais ...
Moi je trouve que la méthode de foetus est pas trop mal, de plus j'ai l'impression que c'est la méthode qu'utilise Microsoft.
En tout ça quand je vois cette méthode je trouve ça très claire.
Code:
1 2 3 4 5 6 7 8 9 10
| DWORD WINAPI GetSecurityInfo(
_In_ HANDLE handle,
_In_ SE_OBJECT_TYPE ObjectType,
_In_ SECURITY_INFORMATION SecurityInfo,
_Out_opt_ PSID *ppsidOwner,
_Out_opt_ PSID *ppsidGroup,
_Out_opt_ PACL *ppDacl,
_Out_opt_ PACL *ppSacl,
_Out_opt_ PSECURITY_DESCRIPTOR *ppSecurityDescriptor
); |