Ceci est une ancienne révision du document !
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|
Lorsque l'on initialise une variable avec une littérale, on fournit deux fois l'information sur le type. Par exemple, dans le code suivant :
int const i { 123 };
Dans ce code, le type de la variable i
et la littérale 123
sont des entiers de type int
.
Si le type de la variable et de la littérale n'est pas parfaitement identique, le compilateur effectue une conversion implicite, pour adapter le type de la littérale en fonction du type de la variable. Lorsque cette conversion implicite ne pose pas de problème et que la littérale peut être convertie dans le type de la variable sans perte d'information, le compilateur ne va pas signaler cette conversion implicite.
#include <iostream> int main() { int const i { 123 }; float const c { 123 }; std::cout << i << std::endl; std::cout << c << std::endl; }
affiche :
123 123
En revanche, si la littérale ne peut pas être convertie dans le type de la variable sans perte d'information, le compilateur signalera le problème. Une perte d'information peut avoir lieu par exemple dans les conversions suivantes :
int i { 123.000 }; // 1 unsigned int ui { -1 }; // 2 char c { 123456789}; // 3
Le compilateur signalera une erreur ou un avertissement (selon les options de compilation) pour signaler la conversion (narrowing). Par exemple, dans le cas d'une conversion d'un double
vers un int
:
#include <iostream> int main() { int const i { 123.456 }; std::cout << i << std::endl; }
affiche :
main.cpp:4:13: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing] int i { 123.456 }; ^~~~~~~ main.cpp:4:13: note: override this message by inserting an explicit cast int i { 123.456 }; ^~~~~~~ static_cast<int>( ) main.cpp:4:13: warning: implicit conversion from 'double' to 'int' changes value from 123.456 to 123 [-Wliteral-conversion] int i { 123.456 }; ~ ^~~~~~~ 1 warning and 1 error generated.
Le premier message donne le message d'erreur de conversion (“type 'double' cannot be narrowed to 'int'” signifie “le type 'double' ne peut pas être réduit dans 'int'”).
À la fin du message d'erreur, le compilateur indique l'option de compilation qui active la vérification des erreurs de conversion -Wc++11-narrowing
. Vous avez déjà vu quelques options de compilation -Wall -Wextra -pedantic
. Vous n'avez utilisez cette nouvelle option, mais pourtant le compilateur a signalé le problème. La raison est que les options de compilation que vous avez utilisé -Wall -Wextra -pedantic
activent d'autres options de compilation, en particulier, -Wc++11-narrowing
.
Le deuxième message propose de réaliser une conversion explicite au lieu d'une conversion implicite. La raison est que autant le compilateur peut supposer qu'une conversion implicite puisse être une erreur, autant une conversion explicite est forcement un choix volontaire du développeur. Vous verrez dans un prochain chapitre l'utilisation de static_cast
.
Le dernier message d'avertissement, prévenant que du fait de la conversion implicite, la littérale 123.456 est changé en littérale entier 123.
On peut légitimement se poser la question de fournir deux fois la même information (le type que l'on souhaite utiliser), surtout que si on n'utilise pas le même type, il y a un risque de conversion implicite et d'erreur.
L'inférence de type consiste à laisser le compilateur définir le type d'une variable, en fonction de l'expression qu'on lui donne pour initialiser une variable. Cette expression peut être une littérale, une autre variable, une expression mathématique, une fonction, etc. La syntaxe pour utiliser l'inférence de type est la suivante :
auto ma_variable = expression;
Le mot-clé auto
remplace le type de la variable et sert à indiquer au compilateur d'utiliser l'inférence de type.
auto
et l'initialisation avec des crochets.
Avec l'initialisation par défaut auto i {};
, il n'a pas d'expression donnée et donc le compilateur n'a pas de moyen de déduire le type qu'il doit utiliser.
Avec l'initialisation avec une valeur auto i { 123 };
, le type déduit n'est pas un entier, mais un tableau d'entier contenant une valeur (ce type de tableau s'appelle “initializer list”). Ainsi, si vous essayez par exemple de réaliser une addition, cela produira une erreur (l'addition d'un tableau “initializer list” et d'un entier n'est pas définie).
#include <iostream> int main() { auto i { 123 }; std::cout << i + 1 << std::endl; }
affichera l'erreur suivante :
main.cpp:5:20: error: invalid operands to binary expression ('std::initializer_list<int>' and 'int') std::cout << i + 1 << std::endl; ~ ^ ~
Vous verrez dans les prochains chapitres comment utiliser les tableaux.
La déduction du type est relativement simple dans le cas d'initialisation par des littérales ou une expression :
auto i = 123; // int auto j = 12 + 45; // int auto k = i; // int auto d = 123.456; // double auto b = true; // bool
Cette écriture présente deux avantages par rapport à la déclaration de variables en explicitant le type. Premièrement, cela évite de devoir écrire le type. Avec des types simples, l'avantage est réduit, mais certains types complexes sont long à écrire et source potentielle d'erreur d'écriture. Vous verrez en particulierpar la suite l'utilisation des itérateurs pour parcourir les conteneurs, dont l'écriture est assez lourde.
Le second avantage est d'avoir un code plus évolutif. En effet, imaginons que l'on réaliser plusieurs calculs numériques complexes. Si on écrit explicitement le type et que l'on souhaite ensuite changer de type utilisé, il faudra modifier plusieurs lignes de code, ce qui diminue la maintenabilité du code.
Par exemple, la formule permet de calculer la fonction de Gauss (loi normale centrée réduite) :
$$ f(x) = \frac 1 { \sqrt {2 \pi} } e ^ { - \frac {value^2} 2 } $$
Le code correspondant est relativement simple :
#include <iostream> #include <cmath> int main() { auto value = 1.0f; // float auto result = std::exp(-value*value/2); std::cout << sizeof(result) << std::endl; }
Dans ce code, le type de la variable value
est déduite de la littérale, le type est donc un float
. De même pour la variable result
. Maintenant, imaginons que l'on réalise que le calcul n'est pas assez précis avec un float
et que l'on souhaite utiliser un type double
. Dans le cas précédent, il suffit simplement de modifier la littérale pour modifier le type des variables value
et result
. Selon le type de la littérale, le code affichera 4 (taille d'un float
) ou 8 (taille d'un double
).
Vous avez déjà vu l'utilisation de using
pour éviter en partie ce problème. L'utilisation de auto
peut être une autre solution. Vous verrez également par la suite la programmation générique, au autre technique permettant au compilateur de déduire les types.
Selon le contexte et les contraintes, l'une des approches sera peut-être préférable à une autre. Le principale pour vous est d'avoir le choix, mais cela implique de se poser que la question du design de son code.
Dans le code précédent, le calcul est en fait faux. En effet, dans la formule donnée, il faut multiplier la valeur retournée par la fonction exponentielle par une constante $\frac 1 { \sqrt {2 \pi} }$ pour obtenir la valeur exacte de la fonction gaussienne. Si on modifie le code pour ajouter cette constante, on ajoute une seconde littérale dans le calcul.
#include <iostream> #include <cmath> int main() { auto value = 1.0f; // float auto result = std::exp(-value*value/2) * 0.398942f; std::cout << sizeof(result) << std::endl; }
Le type de la variable result
n'est pas alors déduit uniquement de la première littérale (utilisée pour initialiser la variable value
), mais de deux littérales. Ici, comme on utilise des float
et des double
dans un calcul, le compilateur va utiliser le type le plus large, pour éviter une perte d'information (donc double
). Pour modifier le type déduit pour la variable result
, il faut donc modifier les deux littérales. On en revient donc au problème de devoir modifier le code à plusieurs endroit, ce qui augmente le risque d'erreur (si on oublie par exemple de modifier une des deux littérales).
La raison est simple : en utilisant auto
, on ne dit pas au compilateur “pour la variable result, utilise le même type que pour la variable value”, mais “déduis le type de la variable result en fonction de l'expression”. Si l'expression ne donne pas exactement le type que l'on avait prévu, on n'aura pas forcement le type attendu.
Heureusement, il existe la fonction decltype
(“declare type”) pour récupérer le type d'une expression et l'utiliser pour définir une variable. L'expression est donnée entre parenthèses :
#include <iostream> #include <cmath> int main() { auto value = 1.0f; // float decltype(value) result = std::exp(-value*value/2) * 0.398942; std::cout << sizeof(result) << std::endl; }
Dans ce code, on a simplement remplacé auto
par decltype(value)
, ce qui permet de dire explicitement au compilateur : “pour la variable result, tu récupères le type de la variable value”.
Cela commence à faire beaucoup de méthodes pour déclarer une variable (et il reste les fonctions génériques, que vous verrez dans les prochains chapitres). Il peut être compliqué de choisir quelle est la meilleure approche à utiliser dans ces conditions. Lorsque vous verrez les fonctions génériques, cette question sera détaillée.
Retenez simplement, pour le moment, qu'un type explicitement écrit dans le code est une ligne de code qu'il faudra modifier manuellement si vous changez d'avis. Un type déduis ne nécessitera pas de correction de votre part (sauf erreur d'écriture).
Le mot-clé auto
permet de déduire le type d'une expression, mais pas les modificateurs comme la constante (mais également les références, que vous verrez par la suite). Ainsi, dans le code précédent :
#include <iostream> #include <cmath> int main() { auto value = 1.0f; // float decltype(value) result = std::exp(-value*value/2) * 0.398942; std::cout << sizeof(result) << std::endl; }
La variable value
n'est pas une constante pour le compilateur et vous pouvez tout à fait modifier cette variable. Si vous avez décidé qu'une variable devait être une constante, il faudra donc utiliser le mot-clé const
, comme vous l'avez fait auparavant.
#include <iostream> #include <cmath> int main() { auto const value = 1.0f; // float decltype(value) result = std::exp(-value*value/2) * 0.398942; std::cout << sizeof(result) << std::endl; }
De son côté, la fonction decltype
permet de récupérer le type complet d'une expression, constance aussi. Dans le code précédent, si la variable value
est déclaré comme constante, alors la variable result
le sera aussi.
auto i = 1; decltype(i) j = i; i += 1; // ok, non const j += 1; // ok, non const auto const k = 1; decltype(k) l = k; k += 1; // erreur, k est const l += 1; // erreur, l est const
Bien sûr, selon le contexte, vous souhaiterez peut-être utiliser decltype
avec un type constant, mais pour déclarer un type non constant. Heureusement, cette situation a été prévue et il est possible de supprimer la constance d'un type déduit avec decltype
. Pour cela, vous devez utiliser la fonction générique remove_const
(“supprimer const”) du fichier d'en-tête <type_traits>
(pour rappel, une fonction générique utilise des chevrons) :
std::remove_const<TYPE>::type
Ainsi, le code précédent peut alors s'écrire :
#include <iostream> #include <cmath> int main() { auto const value = 1.0f; // float std::remove_const<decltype(value)>::type result = std::exp(-value*value/2) * 0.398942; result += 1.0; // ok, result n'est pas constant std::cout << sizeof(result) << std::endl; }
std
: les mots-clés auto
et decltype
sont des fonctionnalités du langage C++, il ne faut pas utiliser std::
. Au contraire, remove_const
est une fonctionnalité de la bibliothèque standard et nécessite donc l'utilisation de std::
.
En cas de doute sur l'utilisation de l'espace de noms, il ne faut pas hésiter à vérifier dans la documentation. Ou vous pouvez lire les messages du compilateur.
La déclaration de la variable result
peut sembler un peu compliqué, mais elle respecte la syntaxe Type Nom = Expression
. Le code std::remove_const<decltype(value)>::type
peut être lu comme “on prend la variable value, on récupère son type avec decltype puis on retire const de ce type”.
Donc, pour résumer, avec auto
il faut ajouter const
si on veut que la variable soit constante. Avec decltype
, il faut retirer const
si on ne veut pas que la variable soit constante. Le tableau suivant résume la situation :
Type | Déclaration | Type déduit |
---|---|---|
T | auto | T |
T const | auto | T |
T | auto const | T const |
T const | auto const | T const |
T | decltype | T |
T const | decltype | T const |
T | remove_const<decltype> | T |
T const | remove_const<decltype> | T |
problème avec “hello, world!” → ne correspond pas à string, mais à une littérale chaine de caractère (const char*). Donc par exemple, pas possible d'écrire :
auto s = "hello, world!"; cout << s.size() << endl; // erreur
Possibilité d'indiquer le type :
auto s = string { 123 }; // string auto c = complex { 123, 456 }; // complex
auto remplace que le type (int, double, etc), pas les modificateurs (const, &, &&). Par exemple pour écrire une constante :
auto const i = 123;
Chapitre précédent | Sommaire principal | Chapitre suivant |
---|