Les paramètres en C++
by Baduit
Mise en contexte
Le C++ est un langage puissant, multi-paradigme avec des fonctionnalités de haut niveau mais permettant aussi de faire du code de bas niveau et de l’encapsuler. Mais cela peut le rendre difficile à appréhender, sans compter des syntaxes qui peuvent paraître plus ou moins barbares aux yeux des non-initiés. Il est temps de corriger ça.
Dans cet article, nous allons parler de comment choisir la meilleure manière de passer des arguments à une fonction, et la meilleure manière de renvoyer la valeur de retour. Cela peut paraitre évident, mais en C++, il y a quelques particularités qui peuvent rendre la chose moins triviale qu’il n’y parait.
Prenons une classe toute simple contenant une std::string et un int, avec des méthodes pour récupérer et modifier ces attributs:
1
2
3
4
5
6
7
8
9
10
11
12
13
class an_amazing_class
{
public:
int get_int() const;
void set_int(int new_int);
const std::string& get_str() const;
void set_str(const std::string& str);
private:
int _int;
std::string _str;
};
On peut voir que les prototypes des deux méthodes concernant la std::string sont assez différentes de celles concernant l’int. Pour l’entier on utilise le type seul comme type de l’argument et type de retour, mais pour la chaine de caractère on rajoute un « const » devant et un « & » après.
Pourquoi ? Qu’est ce que cela veut dire ? Vous commencez peut-être à vous dire que le C++ est compliqué et que vous feriez mieux d’utiliser un autre langage, mais ne vous inquiétez pas : nous allons éclaircir tout ça dans la suite de l’article.
La copie
Commençons par le cas qui semble le plus simple : l’entier, on a juste écrit son type sans fioritures autour. Cela veut donc dire que la valeur utilisée en argument/renvoyée comme valeur de retour est une copie. Concrètement cela veut dire que le constructeur par copie est appelé, une nouvelle variable est créée avec la même valeur, pour un cas aussi simple qu’un entier, cela copie donc la valeur de notre entier dans un nouvel entier qui est envoyé/retourné par la suite.
Note : si c’est un type défini par le langage (les nombres, références, pointeurs, tableaux et les énumérations), il n’y a techniquement pas de constructeur par copie mais le comportement est le même. Source
Les références immutables
Voyons maintenant le « & ». Il signifie qu’il s’agit d’une référence, donc au lieu d’avoir une copie de la variable, on a l’adresse de la variable pour pouvoir y accéder, le mot clef « const » signifie qu’on ne peut pas la modifier. Finalement, cela envoie l’adresse de la variable et cette variable ne peut pas être modifiée.
Comparaison
Pour résumer, la copie crée une nouvelle variable en utilisant le constructeur par copie, la référence immutable utilise l’adresse de la variable pour pouvoir y accéder mais sans pouvoir la modifier.
Quand utiliser quoi ?
Si la variable a une taille plus petite ou à peu près semblable à celle d’un pointeur et qu’il est trivialement copiable (pas d’allocation de mémoire ou de traitements lourd), on préféra utiliser une copie, car la syntaxe est plus simple à lire, que c’est tout aussi rapide en temps d’exécution et qu’il n’y a pas besoin de faire attention à la portée de la variable utilisée.
Cependant, si la variable n’est pas trivialement copiable (un std::vector ou un std::string), si elle n’est simplement pas copiable (std::fstream) ou bien si sa taille est supérieur à celle d’un pointeur, il vaudra mieux utiliser une référence immutable.
Note : En revanche, avec les références immutables, il faut faire attention à ne pas retourner de référence d’une variable locale, car la référence vers cette variable ne sera pas valide au-delà de la portée de la variable (généralement la fin de la fonction). Normalement, votre compilateur devrait vous prévenir si jamais vous tentez de le faire.
Exemple :
1
2
3
4
5
const std::string& do_not_do_this()
{
std::string str = "do not reproduce this at home, it is dangerous";
return str;
}
Note : Dans les boucles for parcourant des conteneurs (comme un std::vector), les mêmes règles s’appliquent.
1
2
3
4
5
6
7
8
9
10
11
12
std::vector<std::string> strings;
for (const std::string& str: strings)
{
// do stuff
}
std::vector<int> integers;
for (int i: integers)
{
// do stuff
}
Quelques cas particuliers
Multithread
Si jamais votre variable est utilisée dans plusieurs threads et que l’un des threads est susceptible de modifier la variable, utiliser une copie peut être une bonne idée au lieu de devoir utiliser un mutex à chaque fois que l’on veut accéder à la variable.
Les pointeurs intelligents (smart pointer)
Si vous voulez utiliser une variable contenue dans un pointeur intelligent (std::shared_ptr par exemple), il vaut mieux utiliser une référence ou un pointeur immutable vers la variable pointée. Source
Les templates
En général, pour les templates, il vaut mieux utiliser une référence immutable à moins d’être sur que le type utilisé sera trivialement copiable et assez petit.
Même avec tes explications j’ai du mal à choisir, comment faire ?
Utiliser une petite bibliothèque utilitaire
J’ai développé une mini-bibliothèque (un fichier header) très simple d’utilisation il y a quelques temps, pour choisir à notre place quand utiliser une copie et quand utiliser une référence immutable : https://github.com/Baduit/CPParam
Exemple
1
2
3
4
5
void my_test_function(Param<std::string> str, Param<double> decimal)
{
std::cout << str << std::endl; // référence immutable
std::cout << decimal << std::endl; // copie
}
Le principe est très simple : Param est un alias templaté ; en fonction du type qu’on lui envoie, il regarde si le type est trivialement copiable et plus petit ou équivalent à la taille du pointeur puis choisit le plus adapté.
Récapitulatif des types les plus courant
- Nombre: Copie
- Énumération: Copie
- Structure/Classe: Référence immutable
- Chaine de caractère: Référence immutable
- Conteneur: Référence immutable
- Template: Référence immutable
- Stream: Référence immutable