C++ : presque toujours auto (AAA)
Rédigé par Nicolas K 1 commentaireLe mot clé auto
est apparu en C++11 mais a parfois encore une mauvaise image, à tort ! Car en effet, il y a beaucoup d’avantages à utiliser auto
au maximum. Cet article va détailler comment s’utilise ce mot clé pour déclarer une variable locale et pourquoi il est toujours préférable de déclarer ses variables locales avec ce mot clé.
Présentation
Le mot clé auto
permet (entre autre) de déclarer une variable locale sans écrire son type :
auto entier{7};
Ici, la variable entier
est un int
qui vaut 7. Mais le C++ reste un langage fortement typé ! La variable ne peut avoir qu’un type tout au long de son existence. Le code suivant ne compile donc pas :
auto entier{7};
entier = "erreur"; // erreur de compilation : entier est un int
Avec le mot clé auto
, le compilateur déduit donc le type de variable à partir de la valeur donnée lors de l’initialisation. Puisque 7 est un entier de type int
, entier est de type int
.
Inutile de préciser que puisque le type est déduit de la valeur d’initialisation, cette initialisation est obligatoire. Le code suivant ne compile donc pas :
auto variable; // erreur de compilation
Enfin, une subtilité à connaître :
Attention : le type déduit correspond au type de la valeur sans les spécificateurs const
, volatile
et &
.
Exemple :
volatile int const& a{7};
auto b{a};
Dans le code précédent, b
est du type int
(et pas volatile int const&
comme a
).
Si on souhaite ajouter des spécificateurs, on peut toujours le faire en les ajoutant à la déclaration :
volatile auto const& b{a};
En anglais, on retrouve parfois le sigle AAA pour Almost Always Auto. En français : Presque toujours auto. L’idée est simple : utiliser au maximum ce mot clé auto
. Mais pourquoi ça ?
Pourquoi toujours déclarer ses variables locales avec auto ?
Voyons les principaux avantages à travers quelques exemples.
- On l’a vu juste au dessus, ça oblige à initialiser ses variables (ce qui est très important, nous le savons tous).
- Ça évite les conversions implicites puisque le type déduit est forcément le type de la valeur. Typiquement :
std::vector vecteur{}; // plusieurs lignes de code qui remplissent le vecteur […] int taille = vecteur.size();
Ce code compile et fonctionne… dans la majorité des cas. Mais le type de retour de
vecteur.size()
n’est pas unint
et rien dans la norme n’impose que la valeur de retour puisse rentrer dans une variable de typeint
. En d’autres termes, selon l’implémentation, ce code peut poser un problème lorsque le vecteur contient beaucoup d’éléments. Il s’agit donc là typiquement du genre d’erreur qui ne se reproduit pas à chaque fois et qui est très difficile à déceler !Avec
auto
, il n’y a plus de problème, la variable prend automatiquement le type de la valeur d’initialisation :auto taille = vecteur.size();
- Toujours dans la rubrique "conversions implicites", une autre conséquence est que ça garantit que la variable est directement initialisée avec la valeur donnée et qu’il n’y a pas d’objet temporaire créé. Prenons un exemple :
ColorHSL getDefaultColor() { return {171,0.50,0.23}; } int main() { Color couleur = getDefaultColor(); // […] suite du code }
Dans l’exemple ci-dessus, l’appel à
getDefaultColor()
va créer un objet temporaire de typeColorHSL
qui va ensuite être converti enColor
(si cette conversion implicite est possible). Si on peut travailler indifféremment avec un objet de typeColorHSL
ou avec un objet de typeColor
dans la suite du code, utiliserauto
permet d’éviter de telles conversions contre-performantes :auto couleur = getDefautColor();
Et si c’est voulu de convertir en Color, on peut toujours le faire explicitement (ce qui permet de mettre en évidence le fait que c’est volontaire) :
auto couleur = Color{getDefaultColor()};
- Ça évite de devoir réfléchir aux types complexes dont on se fiche mais qui sont nécessaires pour certaines variables temporaires (qui n’étaient là avant C++11 que pour des raisons techniques, on est d’accord qu’ils n’apportent rien !). Typiquement les itérateurs :
std::map<int, std::string> maMap{}; // sans auto : std::map<int, std::string>::iterator debut = maMap.begin(); // avec auto : auto debut = maMap.begin();
- Ça permet d’avoir un code plus générique. Dans ce même exemple, si on décide un jour de passer à une map constante :
const std::map<int, std::string> maMap{}; // sans auto : ne compile plus std::map<int, std::string>::iterator debut = maMap.begin(); // avec auto : compile toujours auto debut = maMap.begin();
La déclaration sans
auto
ne compile plus (carmaMap.begin()
retourne maintenant unstd::map<int, std::string>::const_iterator
et non plus unstd::map<int, std::string>::iterator
. Alors que celle avecauto
fonctionne toujours. -
Ça apporte une certaine homogénéité de la syntaxe avec les autres déclarations : nom à gauche, type/valeur à droite.
auto allocationStatique = MaClasse{4}; auto allocationDynamique = new MaClasse(4);
-
Et enfin, c’est également plus rapide à écrire et plus lisible !
Conclusion
auto
pour déclarer ses variables locales apporte au code :
-
robustesse (points 1,2)
-
performance (point 3)
-
maintenabilité (points 4,5,6,7)
Alors pourquoi s’en priver ?