Ceci est une ancienne révision du document !
Adapter le type des variables utilisées permet d'optimiser la mémoire en fonction des besoins, mais cela a un impact sur les valeurs que peut prendre un type donné. Il est donc indispensable d'avoir un moyen pour obtenir des informations détaillées sur les types numériques (valeur minimale, valeur maximale, nombre de chiffres après la virgule, etc).
Pour cela, le C++ propose un ensemble de fonctionnalités dans le fichier d'en-tête limits
. Ces fonctionnalités sont un peu particulières, ce sont des classes génériques, qui utilisent une syntaxe avec des chevrons <>
. Une telle classe, permettant d'obtenir des informations sur un type, est appelée une classe de traits. Vous allez voir dans ce chapitre comment utiliser ce type de classes, leur création sera abordé dans la partie sur la programmation objet.
La syntaxe générale est la suivante :
std::numeric_limits<TYPE>::FONCTION()
Avec TYPE
, qui correspond au type pour lequel vous souhaitez obtenir des informations (par exemple int
, double
, etc), et FONCTION
, qui correspond à l'information que vous souhaitez (par exemple max
pour la valeur maximale et min
pour la valeur minimale).
Par exemple, pour connaître la valeur maximale que peut prendre le type int
, il faut utiliser la syntaxe suivante :
#include <iostream> #include <limits> // N'oubliez pas d'inclure l'en-tête int main() { std::cout << "Max(int) = " << std::numeric_limits<int>::max() << std::endl; std::cout << "Max(int) = " << std::hex << std::showbase << std::numeric_limits<int>::max() << std::endl; }
Le résultat affiché sera :
Max(int) = 2147483647 Max(int) = 0x7fffffff
Cette valeur maximale signifie concrètement que lorsque vous utilisez une variable de type int
pour compter, vous ne pourrez compter que jusqu'à 2147483647. Au delà, le compte ne sera plus correct (en pratique, il reviendra à une valeur négative) :
#include <iostream> int main() { int i { 2147483645 }; std::cout << i << std::endl; ++i; std::cout << i << std::endl; ++i; std::cout << i << std::endl; ++i; std::cout << i << std::endl; ++i; std::cout << i << std::endl; }
affiche :
2147483645 2147483646 2147483647 -2147483648 -2147483647
La syntaxe ++i
permet d'incrémenter la variable i
de +1
. C'est équivalent à écrire : i = i + 1;
ou i += 1;
.
Comme vous le savez déjà, le C++ est permissif, il n'interdira pas ce code. C'est à vous de vérifier que vous ne dépassez pas la valeur maximale possible pour un type.
On peut alors vérifier les valeurs maximales pour les différents types numériques :
#include <iostream> #include <limits> int main() { std::cout << "Types entiers :" << std::endl; std::cout << "Max(short int) = " << std::numeric_limits<short int>::max() << std::endl; std::cout << "Max(unsigned short int) = " << std::numeric_limits<unsigned short int>::max() << std::endl; std::cout << "Max(int) = " << std::numeric_limits<int>::max() << std::endl; std::cout << "Max(unsigned int) = " << std::numeric_limits<unsigned int>::max() << std::endl; std::cout << "Max(long int) = " << std::numeric_limits<long int>::max() << std::endl; std::cout << "Max(unsigned long int) = " << std::numeric_limits<unsigned long int>::max() << std::endl; std::cout << "Max(long long int) = " << std::numeric_limits<long long int>::max() << std::endl; std::cout << "Max(unsigned long long int) = " << std::numeric_limits<unsigned long long int>::max() << std::endl; std::cout << std::endl << "Types réels:" << std::endl std::cout << "Max(float) = " << std::numeric_limits<float>::max() << std::endl; std::cout << "Max(double) = " << std::numeric_limits<double>::max() << std::endl; std::cout << "Max(long double) = " << std::numeric_limits<long double>::max() << std::endl; }
affiche :
Types entiers : Max(short int) = 32767 Max(unsigned short int) = 65535 Max(int) = 2147483647 Max(unsigned int) = 4294967295 Max(long int) = 9223372036854775807 Max(unsigned long int) = 18446744073709551615 Max(long long int) = 9223372036854775807 Max(unsigned long long int) = 18446744073709551615 Types réels: Max(float) = 3.40282e+38 Max(double) = 1.79769e+308 Max(long double) = 1.18973e+4932
Pour le type char
, qui peut être interprété comme un nombre entier ou un caractère, si vous écrivez un code similaire au code précédent, cela n'affichera pas la valeur maximale (std::cout
essayera d'afficher cette valeur comme un caractère). Pour contourner ce problème, il suffit d'enregistrer cette valeur dans une variable de type entier (par exemple int
) et d'afficher cette variable.
#include <iostream> #include <limits> int main() { int const signed_char_max { std::numeric_limits<signed char>::max() }; std::cout << "Max(char) = " << signed_char_max << std::endl; int const unsigned_char_max { std::numeric_limits<unsigned char>::max() }; std::cout << "Max(unsigned char) = " << unsigned_char_max << std::endl; }
affiche :
Max(char) = 127 Max(unsigned char) = 255
Comme vous pouviez vous y attendre, plus un type est représenté par plus d'octets en mémoire, plus sa valeur maximale sera élevée. On peut remarquer aussi que les types long int
et long long int
, qui ont la même taille mémoire (8 octets), ont la même valeur maximale. Vous pouvez aussi noter que les types unsigned
ont des valeurs maximales plus grandes que le type signé correspondant.
char
n'est pas définie par la norme et peut changer selon le système. De plus, ce type peut représenter un caractère et un entier.
Pour limiter les risques de confusion, dans ce cours, nous utiliserons char
pour indiquer un caractère et signed char
ou unsigned char
lorsque l'on veut explicitement faire référence à la représentation entière.
Notez bien que ce n'est qu'une convention d'écriture dans ce cours. Si char
est signé, il sera équivalent à signed char
en termes de comportement. De même s'il n'est pas signé, il sera équivalent à unsigned char
.
Pour obtenir les valeurs minimales d'un type, il faut utiliser les fonctions min
et lowest
à la place de max
. Cependant, ces fonctions ne retournent pas exactement la même chose, selon que vous l'utiliser avec un type entier ou un type réel.
Commençons par les types entiers. Dans ce cas, les deux fonctions retournent la même valeur, qui correspond à la plus petite valeur que peu prendre un type. Pour les types non signées, cette valeur sera toujours 0 et pour les types signés, cette valeur sera -max() - 1
:
#include <iostream> #include <limits> int main() { int const signed_char_min { std::numeric_limits<signed char>::min() }; std::cout << "Min(char) = " << signed_char_min << std::endl; std::cout << "Min(short int) = " << std::numeric_limits<short int>::min() << std::endl; std::cout << "Min(int) = " << std::numeric_limits<int>::min() << std::endl; std::cout << "Min(long int) = " << std::numeric_limits<long int>::min() << std::endl; }
affiche :
Min(char) = -128 Min(short int) = -32768 Min(int) = -2147483648 Min(long int) = -9223372036854775808
Concrètement, cela veut dire, pour une variable de type int
par exemple, que vous pouvez compter de -2147483648 à 2147483647. Et pour un type unsigned int
, que vous pouvez compter de 0 à 4294967295.
Dans le cas des nombres réels, les fonctions min
et lowest
ne retournent pas la même valeur :
min
retourne la plus petite valeur positive non nulle (la plus petite valeur proche de 0) ;lowest
retourne la plus petite valeur représentable, donc généralement -max
.
En mathématique, quelque soit $x$ appartenant à l'ensemble des nombres réels positifs non nul $\mathbb{R}^*_+$, il existe toujours un nombre plus petit que $x$ (on peut démontrer cela facilement, on montrant que $\frac x 2$ est strictement compris entre 0 et $x$). Ce qui revient à dire qu'il n'existe pas de plus petit nombre positif non nul.
En informatique, la situation est différente. Un nombre en mémoire est représenté par un nombre fini de bits, par exemple 32 pour float
ou 64 bits pour double
. Cela veut dire qu'il est possible représenter qu'un nombre limité de valeur possible sur un ordinateur et donc qu'il n'existe pas toujours un nombre réel $\frac x 2$. Dit autrement, cela veut dire qu'il existe une valeur minimale positive.
Il est très important de garder en mémoire que les nombres réels dans un programme ne sont qu'une REPRÉSENTATION des nombres réels tels qu'ils sont conçus en mathématique. Cela aura une importance particulière lorsque vous réaliserez des calculs numériques.
Cette valeur minimale positive est celle obtenue avec la fonction min
.
#include <iostream> #include <limits> int main() { std::cout << "Min(float) = " << std::numeric_limits<float>::min() << std::endl; std::cout << "Min(double) = " << std::numeric_limits<double>::min() << std::endl; std::cout << "Min(long double) = " << std::numeric_limits<long double>::min() << std::endl; }
affiche :
Min(float) = 1.17549e-38 Min(double) = 2.22507e-308 Min(long double) = 3.3621e-4932
Pour obtenir la plus petite valeur représentable , vous pouvez utiliser la fonction lowest
.
#include <iostream> #include <limits> int main() { std::cout << "Lowest(float) = " << std::numeric_limits<float>::lowest() << std::endl; std::cout << "Lowest(double) = " << std::numeric_limits<double>::lowest() << std::endl; std::cout << "Lowest(long double) = " << std::numeric_limits<long double>::lowest() << std::endl; }
affiche :
Lowest(float) = -3.40282e+38 Lowest(double) = -1.79769e+308 Lowest(long double) = -1.18973e+4932
climits
, tel que INT_MIN
, INT_MAX
, DBL_MIN
, DBL_MAX
, mais elles n'offrent pas les mêmes garanties concernant les types. Elles sont fournies à titre de rétro-compatibilité avec le C et les anciens codes C++ et ne sont donc pas recommandées. Voir C numeric limits interface pour plus de détails.
Il est également possible d'obtenir des informations sur les types, autre que les valeurs limites qu'elles peuvent prendre (lowest
, min
et max
) ou la taille en mémoire (sizeof
). Par exemple, pour savoir si un type est signé ou non, savoir si un type est entier ou réel, etc. L'ensemble de ces fonctionnalités est dans le fichier d'en-tête type_traits
.
Fonction | Rôle | Exemple |
---|---|---|
is_arithmetic | Teste si un type représente un nombre | std::is_arithmetic<int>::value |
is_integral | Teste si un type représente un nombre entier | std::is_integral<int>::value |
is_floating_point | Teste si un type représente un nombre réel | std::is_floating_point<int>::value |
is_signed | Teste si un type est signé | std::is_signed<int>::value |
is_unsigned | Teste si un type n'est pas signé | std::is_unsigned<int>::value |
is_const | Teste si un type est constant | std::is_const<int>::value |
Ces différents code retournent un booléen, vous pouvez donc les afficher avec std::cout
(en utilisant la directive std:::boolalpha
si vous voulez qu'il affiche true
ou false
en toutes lettres) et utiliser les opérateurs booléens déjà vus (inverse !
, AND &&
et OR ||
).
#include <iostream> #include <type_traits> // N'oubliez pas d'inclure l'en-tête int main() { std::cout << std::boolalpha; std::cout << "is_arithmetic(int) = " << std::is_arithmetic<int>::value << std::endl; std::cout << "is_arithmetic(bool) = " << std::is_arithmetic<bool>::value << std::endl; std::cout << std::endl; std::cout << "is_integral(int) = " << std::is_integral<int>::value << std::endl; std::cout << "is_integral(float) = " << std::is_integral<float>::value << std::endl; std::cout << std::endl; std::cout << "is_floating_point(float) = " << std::is_floating_point<float>::value << std::endl; std::cout << "is_floating_point(int) = " << std::is_floating_point<int>::value << std::endl; std::cout << std::endl; std::cout << "is_signed(int) = " << std::is_signed<int>::value << std::endl; std::cout << "is_signed(unsigned int) = " << std::is_signed<unsigned int>::value << std::endl; std::cout << std::endl; std::cout << "is_unsigned(unsigned int) = " << std::is_unsigned<unsigned int>::value << std::endl; std::cout << "is_unsigned(int) = " << std::is_unsigned<int>::value << std::endl; std::cout << std::endl; std::cout << "is_const(const int) = " << std::is_const<const int>::value << std::endl; std::cout << "is_const(int) = " << std::is_const<int>::value << std::endl; }
affiche :
is_arithmetic(int) = true is_arithmetic(bool) = true is_integral(int) = true is_integral(float) = false is_floating_point(float) = true is_floating_point(int) = false is_signed(int) = true is_signed(unsigned int) = false is_unsigned(unsigned int) = true is_unsigned(int) = false is_const(const int) = true is_const(int) = false
Pour terminer, il peut être intéressant de pouvoir comparer deux types, pour vérifier s'ils sont équivalent ou non. Pour cela, le C++ fournit la classe générique std::is_same
, qui s'utilise avec la syntaxe suivante :
std::is_same<TYPE1, TYPE2>::value
Par exemple, pour comparer les types char
:
#include <iostream> #include <type_traits> int main() { std::cout << std::boolalpha; std::cout << "is_signed(char) = " << std::is_signed<char>::value << std::endl; std::cout << "is_unsigned(char) = " << std::is_unsigned<char>::value << std::endl; std::cout << "is_same(char, signed char) = " << std::is_same<char, signed char>::value << std::endl; std::cout << "is_same(char, unsigned char) = " << std::is_same<char, signed char>::value << std::endl; }
affiche :
is_signed(char) = true is_unsigned(char) = false is_same(char, signed char) = false is_same(char, unsigned char) = false
char
est un cas particulier. Même s'il est signé ou non signé, c'est un type différent de signed char
et unsigned char
. Même s'il aura généralement le même comportement, en particulier lorsqu'il est utilisé avec std::cout
.
Dans ce chapitre, vous avez vu une utilisation simple des classes numeric_limits
et is_same
(qui sont appelées “classes de traits”, d'où le nom du fichier d'en-tête). Mais en réalité, ces fonctionnalités sont beaucoup plus utiles et puissantes que ce qui est présenté ici. En effet, par la suite, vous verrez qu'il est possible de créer du code C++ qui produit du code C++ en utilisant ce type de fonctionnalités. Cela s'appelle de la méta-programmation et c'est l'une des forces du C++.