Outils d'utilisateurs

Outils du Site


inference_de_type

Ceci est une ancienne révision du document !


Chapitre précédent Sommaire principal Chapitre suivant

L'inférence de type

Conversion implicite de types

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.

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

  1. une littérale de type réel dans une variable de type entier ;
  2. une littérale négative dans une variable non signée ;
  3. une littérale trop grande dans une variable de type ne pouvant contenir cette valeur.
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 :

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

Inférence de type

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.

Il n'est pas possible d'utiliser les syntaxes de déclaration des variables avec 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).

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

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

Obtenir le type d'une expression

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.

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

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

auto, decltype et const

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 :

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

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

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

Attention à l'utilisation de l'espace de noms 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

Vérifier les types déduis

Le cas particulier des chaînes de caractères

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
inference_de_type.1402483193.txt.gz · Dernière modification: 2014/06/11 12:39 par gbdivers