Outils d'utilisateurs

Outils du Site


informations_sur_les_types

Obtenir des informations sur les types

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ée 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).

Valeur maximale

Par exemple, pour connaître la valeur maximale que peut prendre le type int, il faut utiliser la syntaxe suivante :

main.cpp
#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) :

main.cpp
#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 :

main.cpp
#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.

main.cpp
#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.

Comme indiqué précédemment, le signature du type 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.

Valeur minimale

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'utilisez 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 peut prendre un type. Pour les types non signés, cette valeur sera toujours 0 et pour les types signés, cette valeur sera -max() - 1 :

main.cpp
#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.

Mathématiques discrète et continue

En mathématique, quelque soit $x$ appartenant à l'ensemble des nombres réels positifs non nuls $\mathbb{R}^*_+$, il existe toujours un nombre plus petit que $x$ (on peut démontrer cela facilement, en 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 n'est possible de représenter qu'un nombre limité de valeurs possibles 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.

main.cpp
#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.

main.cpp
#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

Il existe également des constantes définies dans l'en-tête 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.

Informations sur les types

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 codes 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 ||).

main.cpp
#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 équivalents 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 :

main.cpp
#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

Ce résultat peut paraître surprenant, mais il faut bien comprendre que 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++.

informations_sur_les_types.txt · Dernière modification: 2019/07/01 11:47 par sebastien