Évolution du mot clef auto
Sommaire- Automatic storage duration specifier (avant C++11) (obsolète)
- Placeholder type specifiers (depuis C++11)
- Trailing return type (depuis C++11)
- auto as a return type (depuis C++14)
- decltype(auto) (depuis C++14)
- Structured binding declaration (depuis C++17)
- auto in template parameters (depuis C++17)
- Abbreviated function template (depuis C++20)
- auto cast (depuis C++23)
- AAA (Almost Always Auto) (avant C++17)
- AA (Always Auto) (depuis C++17)
Le mot clef auto
, ses avantages et ses différents comportements selon la version et le contexte.
Automatic storage duration specifier (avant C++11) (obsolète)
En langage C, auto
(pour “Automatic Storage Duration”) sert à spécifier qu’une variable a une portée locale à son bloc de code (scope).
C’est à dire, faire que la variable soit supprimée à la sortie du scope, contrairement aux variables globales ou statiques.
Dans les premières versions du langage C (Le “C K&R”), il était obligatoire de déclarer explicitement les variables locales avec auto
.
Dès l’arrivée du C ANSI (C89) en 1989, le comité de standardisation créé pour l’occasion décide de rendre les variables locales auto
par défaut et le mot clef auto
optionnel, le rendant par conséquent redondant et inutile à renseigner explicitement.
Il reste cependant supporté dans les versions suivantes du C pour des raisons de rétrocompatibilité.
En C++, auto
avait la même signification jusqu’au C++11.
A partir de cette version, le mot clef auto
se voit attribuer une autre signification pour faire de l’inférence de types.
Placeholder type specifiers (depuis C++11)
Dès C++11, le mot clef auto
permet de faire de l’inférence de types.
En écrivant auto
à la place du type d’une variable, le type de la variable est déduit à partir de la valeur à droite du signe égal.
Les literals facilitent le typage lors de l’initialisation de variables.
Cette déduction de type est faite à la compilation (typage statique) et ne permet pas de faire du typage dynamique.
Contrairement à var
/let
en JavaScript, le mot clef auto
ne permet pas à une variable de changer de type en cours de route. Son type reste fixe.
Par défaut, auto
ne récupère pas les propriétés cvref (const
/volatile
/reference
) de la valeur qui lui est assignée.
Il faut penser à bien les renseigner pour éviter les copies inutiles.
Le *
des raw-pointers est bien déduit par le mot clef auto
, mais il est préférable de l’expliciter en écrivant auto*
.
L’usage explicite de auto*
permet de signaler de manière claire que vous travaillez avec des pointeurs, ce qui peut améliorer la lisibilité du code.
A noter que l’écriture
auto string = std::string{"Hello World"};
est appelée “auto to track”.
Elle consiste à forcer la variablestring
à adopter le type à droite du signe égal (std::string
).
L’écritureauto c_string0 = std::data(string);
est quant à elle nommée “auto to stick”.
Elle consiste à déduire le type de la variablec_string0
en fonction du type retourné par la fonctionstd::data
.
Common type deduction
Lorsqu’un type dépend de plusieurs expressions, l’utilisation de auto
permet au compilateur de déduire le type commun entre les différentes expressions possibles.
Par exemple, dans le cas d’une ternaire où c
peut se voir attribuer la valeur de a
ou de b
selon une condition:
Si a
et b
sont de types différents, le mot clef auto
permet de déduire automatiquement le type commun de ces deux expressions.
Équivaut à:
Ici, le type commun de int
et double
est le type double
, car un double
peut être construit à partir d’un int
mais l’inverse n’est pas possible directement.
Typer une lambda
auto
permet également de typer une lambda.
En effet, en C++ chaque lambda a un type unique qui lui est propre, et ce, même si plusieurs lambdas ont la même signature.
Ecrire explicitement leur type est donc impossible.
L’utilisation du mot clef auto
est le seul moyen de typer une variable contenant une lambda:
Attention, le mot clef auto
est différent pour les paramètres de fonctions. On aborde ce point plus bas.
Trailing return type (depuis C++11)
En C++, le type de retour des fonctions est écrit au début de leur définition/déclaration:
Dans d’autres langages, le type est la plupart du temps spécifié à la fin de leur définition/déclaration:
En Python:
En Typescript:
Pour revenir au C++, le trailing return type permet de spécifier le type de retour des fonctions à la fin de leur définition/déclaration (depuis C++11):
Cette écriture permet entre autre de définir un type de retour qui dépend des arguments de la fonction.
Si vous n’êtes pas familiers avec les templates, passez faire un tour ici. Et pour
decltype(expression)
, c’est ici.
Cela n’est pas possible avec l’ancienne écriture des fonctions:
<source>:5:10: error: ‘lhs’ was not declared in this scope;
<source>:5:16: error: ‘rhs’ was not declared in this scope;
Le compilateur comprend les déclarations dans l’ordre dans lequel il les lit. Et comme il lit les fichiers de haut en bas et de gauche à droite, il ne connait pas encore lhs
et rhs
à l’instant où on les utilise dans decltype(lhs + rhs)
.
Cette nouvelle syntaxe apporte aussi une uniformisation entre la syntaxe des fonctions et celle des lambdas.
Les lambdas (C++11) s’écrivent de la façon suivante, avec le type de retour à droite:
A noter qu’ici, auto
n’est pas le type de la valeur de retour de la lambda, mais le type de la lambda elle-même.
On en a parlé dans le précédent point.
En résumé, utiliser
auto
avec le trailing return type permet d’uniformiser la manière dont les types de retour sont déclarés et assure une meilleure lisibilité, surtout dans les fonctions dont le type de retour dépend des paramètres.
Cette pratique est recommandée en C++ moderne.
auto
as a return type (depuis C++14)
A partir de C++14, on peut laisser le compilateur déduire le type de retour à partir du return
de la fonction, en le ne renseignant plus explicitement:
Cependant, ce n’est pas une écriture que vous verrez couramment car elle comporte des risques et qu’elle ne peut pas toujours s’appliquer.
Déjà, retourner auto
est suffisant dans les définitions, mais pas dans les déclarations car elles n’ont pas accès au corps de la fonction pour déduire son type de retour.
Sum.h
Sum.cpp
Il arrive que la fonction contienne plusieurs return
.
Contrairement aux ternaires, sur lesquelles auto
déduit automatiquement le type commun, auto
comme type de retour exige que toutes les valeurs retournées partagent exactement le même type.
Ceci provoque une erreur de compilation, bien qu’un type commun existe (std::string_view
)
<source>:9:29: error: inconsistent deduction for auto return type: ‘const char*’ and then ‘std::basic_string_view
'
L’ambiguïté peut être résolue en précisant explicitement le type:
Le compilateur tente maintenant de construire un std::string_view
à partir du const char*
retourné. Ce qui est fait via un appel implicite à un constructeur de std::string_view
.
Attention toutefois: Evitez les conversions implicites autant que possible, c’est une mauvaise pratique. Le code précédemment n’est là qu’a des fins de démonstration pour montrer les problèmes que l’on peut rencontrer avec le auto as a return type
Notez qu’il est possible de retourner auto
en suivant le trailing return type pour respecter l’uniformisation:
Ici, il n’y a pas de redondance du mot clef auto
.
Seul celui à droite désigne le type de retour de la fonction.
Celui de gauche est simplement nécessaire pour l’écriture du trailing return type.
Ici, il n’y a aucun intérêt autre que l’uniformisation d’écrire
-> auto
.
Ecrire simplementauto sum(Lhs lhs, Rhs rhs)
revient au même.
decltype(auto)
(depuis C++14)
Contrairement à auto
, decltype(auto)
permet de préserver les propriétés cvref (const
/volatile
/reference
) d’une expression.
decltype(auto)
est particulièrement utile lorsqu’il est nécessaire de préserver la nature exacte de l’expression retournée, que ce soit une référence ou un type const:
decltype(auto)
est aussi utilisable pour initialiser une variable en conservant les propriétés cvref de la valeur assignée:
Avec cette initialisation de variable, il est possible de faire ceci:
Structured binding declaration (depuis C++17)
Les structured binding declaration permettent de décomposer les valeurs stockées dans un conteneur.
Un certain nombre de conteneurs sont supportés (dont des conteneurs standards), dont les structures et classes que vous créez.
Structure ou classe
Les variables membre publiques sont accessibles depuis l’extérieur d’une struct
/class
grace aux structured binding declaration:
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Position2d
{
int x;
int y;
};
auto main() -> int
{
auto position = Position2d{10, 15}; // Construction d'un Position2d avec x vallant 10 et y vallant 15
auto [x, y] = position; // Extraction des variables membre de Position2d
std::print("{} {}\n", x, y); // Affiche: "10 15\n"
}
La destructuration doit respecter l’ordre des paramètres.
Leur nom n’a pas d’importance, il peut être changé. Par exemple:auto [foo, bar] = position;
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Position2d
{
int x;
int y;
};
auto main() -> int
{
auto position = Position2d{10, 15};
auto [a, b] = position;
std::print("{} {}\n", a, b); // Affiche: "10 15\n" malgré l'utilisation de noms de variables différents
}
Ce n’est pas parce qu’il y a écrit qu’une seule fois auto
devant une structured binding declaration que les variables doivent partager le même type.
Chaque variable peut avoir un type différent.
1
2
3
4
5
6
7
8
9
10
11
12
13
struct Person
{
std::string name;
unsigned int birthYear;
};
auto main() -> int
{
auto person = Person{"Bjarne Stroustrup", 1950};
auto [name, birthYear] = person;
std::print("{} est né en {}\n", name, birthYear);
}
Les structured binding declarations supportent les propriétés cvref, permettant de modifier les données contenues dans le conteneur, ou d’éviter des copies:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Person
{
std::string firstName;
std::string lastName;
};
auto main() -> int
{
auto person = Person{
.firstName = "Bjarne",
.lastName = "Stroustrup"
};
const auto& [firstName, lastName] = person; // firstName et lastName sont récupérés par références constantes
std::print("{} {}\n", firstName, lastName);
}
Tableau
1
2
3
4
5
6
7
8
9
auto main() -> int
{
int position[2];
position[0] = 10;
position[1] = 15;
auto [x, y] = position;
std::print("{} {}\n", x, y);
}
Notez que cette écriture c-like des tableaux est à éviter en C++. Préférez l’utilisation de
std::array
.
std::array
1
2
3
4
5
6
7
auto main() -> int
{
auto position = std::array<int>{10, 15};
auto [x, y] = position;
std::print("{} {}\n", x, y);
}
std::pair
Grace à std::pair
il est possible d’obtenir les clefs et valeurs dans une range-based for loop sur une std::map
/std::unordered_map
.
std::tuple
auto
in template parameters (depuis C++17)
Si vous n’êtes pas familiers avec les templates, passez faire un tour ici.
Vous avez surement remarqué que certains templates prennent des valeurs, au lieu de prendre des types.
Par exemple:
Ici, on instancie un std::array
contenant 3 éléments de type int
. Le nombre d’éléments est renseigné en template.
Le passage de valeur en template est possible en écrivant le type accepté à la place de typename
/class
dans la template (template<std::size_t>
):
Pour plus de généricité, il est également possible de le définir avec auto
(template<auto>
).
Ici, auto
sert à indiquer une valeur en template qui sera déduite à l’instantiation.
Equivaut à:
template<auto>
accepte toute constant expression, c’est à dire toute valeur connue à la compilation (integral, pointer, pointer to member, enum, lambda, constexpr object).
template<auto>
ne supporte pas le typedouble
avant C++20.
Utilisé dans une variadic, chaque valeur passée en template peut avoir son propre type:
Abbreviated function template (depuis C++20)
Les templates ont toujours été très verbeuses.
Depuis C++20, il est possible d’utiliser auto
comme syntaxe alternative aux templates, améliorant grandement leur lisibilité:
Attention, derrière ses airs de placeholder type specifiers, il s’agit ici bien de types templatés.
Une template n’est pas toujours souhaitable. Dans cette situation il faut n’utiliserauto
que si une template est souhaitée.
Notez aussi que les deux arguments de
auto sum(auto lhs, auto rhs) -> auto
auront chacun leur propre type template.
Ils ne partageront pas un type template commun.
Ca équivaut àtemplate<class T1, class T2> auto sum(T1 lhs, T2 rhs) -> auto
Pas à:template<class T> auto sum(T lhs, T rhs) -> auto
Comme avec les templates, il est toujours possible de faire des variadic template avec auto
:
Lorsque templates et paramètres auto
sont combinés, cela équivaut à avoir les types des paramètres auto
après les templates:
Equivaut à:
auto cast (depuis C++23)
Une manière générique d’obtenir la copie d’un objet en C++ est auto variable = x;
, mais une telle copie est une lvalue.
auto(a)
(ou auto{x}
) permet d’en obtenir une copie sous forme de prvalue, ce qui peut être utile pour transmettre cet objet en paramètre à une fonction.
AAA (Almost Always Auto) (avant C++17)
Le principe AAA (Almost Always Auto) a vu le jour dès le C++11 pour encourager l’utilisation d’auto
par défaut.
Comme nous venons de le voir, auto
apporte de nombreux avantages, aussi bien pour la lisibilité et l’apport de nouvelles fonctionnalités.
Quelques avantages notables à utiliser auto
:
-
Force l’initialisation des variables, évitant au développeur un oubli d’initialisation (
int i;
), évitant ainsi des erreurs - Évite les conversions implicites lors de l’initialisation des variables (
float f = 1;
: conversion implicite de int vers float) - Plus agréable à écrire pour les types longs (Par exemple les itérateurs)
- Couplé à l’initialisation uniforme, il contribue à réduire la charge mentale causée par les multiples façons d’écrire la même chose. Le C++ devient un langage beaucoup plus lisible et abordable.
- Le type est déjà renseigné (ou déduit) à droite du signe égal, pas de redondance en l’écrivant aussi à gauche.
- Les templates deviennent beaucoup plus lisibles
-
auto
est le seul moyen de typer une lambda
Mais il reste un problème:
Dans l’écriture suivante, le compilateur n’est pas tenu de considérer la ligne comme étant une simple initialisation de variable:
Le compilateur peut considérer cette instruction comme étant la création d’une valeur, puis son déplacement dans la variable string
. Engendrant un léger surcoût.
Il ne faut cependant pas négliger les capacités d’optimisation des compilateurs, qui la plupart du temps parviennent à supprimer le coût de ces déplacements.
Ce surcoût est généralement considéré comme négligeable, sauf dans certains cas où l’opération est coûteuse:
std::array
étant un type trivial, son déplacement fait une copie, représentant là aussi un surcoût.
Ici aussi, on peut décider de ne pas utiliser auto
pour éviter ce surcoût.
Dans certains cas, l’écriture avec auto
est même impossible. Lorsqu’un type est non-copyable ET non-movable:
Ceci explique le Almost
dans Almost Always Auto
. On est passé à ça 🤏 d’avoir une règle d’écriture uniforme.
Certains développeurs préfèrent utiliser auto
avec parcimonie, en remplacement de types particulièrement verbeux (notamment les iterateurs).
D’autres prônent son utilisation quasi systématique, comme Scott Meyers et Herb Sutter.
Certains seraient même tentés de ne jamais utiliser auto
pour éviter ce genre de problème, et passer à côté de tous les autres avantages qu’il apporte.
Mais c’est alors que…
AA (Always Auto) (depuis C++17)
En C++17, le langage garanti la copy elision, faisant disparaitre les surcoûts que vous venons de voir, et rendant l’utilisation de auto
possible même sur des types qui ne sont ni copyables, ni movables.
La copy elision est une optimisation qui élimine la création et la copie d’objets temporaires (prvalue). Au lieu de créer une copie intermédiaire, l’objet est directement construit à l’emplacement final.
Suite à ce changement dans le langage, Herb Sutter soutient le passage de AAA à AA.
A votre tour de prendre le pas et d’adopter auto
dans vos projets.
Aller plus loin: