Outils d'utilisateurs

Outils du Site


inference_de_type

La déduction de type

Conversion implicite de types

Lorsque vous définissez une variable, vous pouvez l'initialiser avec une littérale de même type que le type de la variable ou avec un type différent. Si le type de la variable et de la littérale ne sont pas parfaitement identiques, le compilateur effectue une conversion implicite du type de la littérale vers le type de la variable. Lorsque cette conversion implicite ne pose pas de problème et que la littérale peut être convertie vers 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 vers le type de la variable sans perte d'information, le compilateur signale cette conversion implicite (narrowing). Une perte d'information peut avoir lieu par exemple dans les conversions suivantes :

  • une littérale de type réel dans une variable de type entier ;
  • une littérale signée (qui peut être positive ou négative) dans une variable non signée (qui ne peut être que positive) ;
  • une littérale trop grande dans une variable de type ne pouvant pas contenir cette valeur.
int i { 123.0 };        // double dans un int
unsigned int ui { -1 }; // signed dans un unsigned 
char c { 123456789 };   // int dans un char

Le compilateur signale une erreur ou un avertissement (selon les options de compilation) pour signaler la conversion implicite. Par exemple, dans le cas d'une conversion d'un double vers un int :

main.cpp
#include <iostream>
 
int main() {
    const int i { 123.456 };
    std::cout << i << std::endl;
}

affiche :

main.cpp:4:19: error: type 'double' cannot be narrowed to 'int' in initializer list [-Wc++11-narrowing]
    const int i { 123.456 };
                  ^~~~~~~
main.cpp:4:19: note: insert an explicit cast to silence this issue
    const int i { 123.456 };
                  ^~~~~~~
                  static_cast<int>( )
main.cpp:4:19: warning: implicit conversion from 'double' to 'int' changes value from 123.456 to 
123 [-Wliteral-conversion]
    const 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 pas utilisé cette nouvelle option, mais pourtant le compilateur a signalé le problème. La raison est que les options de compilation que vous avez utilisées -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 le compilateur ne peut deviner si la conversion est une erreur ou un choix volontaire du développeur. Pour lever cette ambiguïté, il demande donc au développeur d'écrire explicitement cette conversion. Vous verrez dans un prochain chapitre l'utilisation de static_cast pour ce cas d'utilisation.

Le dernier message d'avertissement prévient que du fait de la conversion implicite, la littérale 123.456 est arrondie et la variable est initialisée avec la valeur 123.

Déduction de type

Utiliser des types différents pour une variable et une littérale lors de l’initialisation peut poser des problèmes. Il est donc logique d'appliquer (sauf cas particuliers) la bonne pratique qui consiste à utiliser le même type pour les deux. Mais dans ce cas, vous pouvez légitimement vous poser la question suivante : pourquoi devoir définir deux fois le type, au risque que cela pose problème ? Ne serait-il pas possible de le définir une seule fois ?

Cela est possible en C++, grâce à la déduction de type, qui permet au compilateur de déduire automatiquement le type d'une variable à partir d'une expression. Il existe trois formes possibles pour définir une variable en C++ utilisant la déduction de type, basées sur une même syntaxe :

<EXPRESSION> IDENTIFIANT = VALEUR;

Le type de la variable est remplacé par <EXPRESSION>, qui peut correspondre à l'un des trois mots-clés suivants :

  • auto : déduit le type à partir de VALEUR, sans conserver les modificateurs de types (const, etc.) ;
  • decltype(auto) : déduit le type à partir de VALEUR, en conservant les modificateurs de types;
  • decltype(expression) : déduit le type d'une expression.

Cette écriture présente de nombreux avantages par rapport à la définition d'une variable sans déduction. Premièrement, cela évite de devoir écrire le type. Avec des types simples, l'avantage est réduit, mais certains types complexes sont longs à écrire et source potentielle d'erreur lors de leur écriture. Vous verrez en particulier par la suite l'utilisation des itérateurs pour parcourir les collections, dont l'écriture est assez lourde.

Un deuxième avantage est d'avoir un code plus évolutif. Imaginez que vous réalisez plusieurs calculs numériques complexes. Si vous écrivez explicitement le type de toutes vos variables et que vous souhaitez ensuite utiliser un autre type, vous devrez modifier l'ensemble de votre code pour utiliser le nouveau type. En utilisant la déduction, vous laissez le compilateur s'occuper de cette tâche.

Cependant, la déduction de type n'est pas toujours utilisable, c'est pour cela qu'il faut connaître les deux syntaxes pour définir une variable.

Attention au type des littérales

Les littérales ne sont pas modifiables, mais la norme C++ définit les types des littérales sans le mot-clé const. Le type de la littérale 123 par exemple sera int et pas const int.

Le mot-clé auto

Commençons par détailler le mot-clé auto. Ce mot-clé indique au compilateur qu'il doit utiliser le type de VALEUR pour le type de la variable, sans conserver les modificateurs comme const (et les références, mais vous verrez cela dans le chapitre sur les fonctions). Par exemple, dans le code suivant :

auto i = 123;

La littérale 123 est de type entier int, le type de la variable i sera donc int aussi. Pour écrire une constante en utilisant la déduction de type avec auto, il faut donc ajouter explicitement le mot-clé const :

auto i = 123;

Dans ce cas, le type déduit par auto est aussi int, mais du fait de la présence de const dans la définition, le type de la variable i sera const int.

Voici quelques exemples supplémentaires :

main.cpp
int main() {
    const auto i = 123;    // 123 est une littérale de type 
                           // int, donc auto déduit le type int.
                           // auto est précédé de const
                           // donc i est de type const int.
 
    const auto j = i;      // i est une variable de type const int,
                           // auto déduit le type int.
                           // auto est précédé de const
                           // donc j est de type const int.
 
    const auto x = 12.34;  // 12.34 est une littérale de type
                           // double, auto déduit le type double.
                           // auto est précédé de const
                           // donc x est de type const double
 
    const auto c = 'a';    // 'a' est une littérale de type
                           // char, auto déduit le type char.
                           // auto est précédé de const
                           // donc c est de type const char
}

Le mot-clé decltype(auto)

La syntaxe avec decltype(auto) est similaire à celle avec auto, mais conserve les modificateurs de type comme const.

Par exemple, dans certains cas, vous souhaiterez définir une constante à partir d'une autre constante, dont la valeur n'est pas modifiée après initialisation. Et dans d'autres cas, vous souhaiterez créer une variable, initialisée avec la valeur d'une constante, que vous pourrez modifier.

Les deux syntaxes auto et decltype(auto) sont complémentaires et vous permettent d'exprimer dans le code quelles sont vos intentions.

main.cpp
auto i = ...;            // i récupère le type de l'expression, mais n'est pas const.
                         // Vous souhaitez pouvoir modifier sa valeur.
 
const auto j = ...;      // j est une constante, quel que soit le type de l'expression.
 
decltype(auto) k = ...;  // k sera exactement du même type que l'expression.

En pratique, pour le moment, ces différentes syntaxes peuvent vous sembler très proches. Lorsque vous déclarez une variable, vous savez si vous voulez qu'elle soit constante ou non, vous n'avez pas besoin de laisser le compilateur choisir s'il doit utiliser const ou pas. Vous n'aurez donc, dans un premier temps, besoin de n'utiliser que auto et const auto.

Cependant, n'oubliez pas qu'il existe d'autres modificateurs que const, que vous verrez par la suite (dans la partie de ce cours sur les fonctions). L'utilisation de auto et decltype(auto) prendra tout son sens à ce moment-là. Nous reviendrons sur ces syntaxes et leurs subtilités d'utilisation le moment venu.

Le tableau suivant résume les différentes syntaxes possibles et les types déduits correspondants (T représente un type fondamental, comme int, double, etc.)

Type Déclaration Type déduit
T auto T
const T auto T
T const auto const T
const T const auto const T
T decltype(auto) T
const T decltype(auto) const T

Le mot-clé decltype

Alors que les mots-clés auto et decltype(auto) utilisent automatiquement l'expression qui se trouve à droite de l'opérateur d'affectation =, le mot-clé decltype permet d'écrire directement une expression qui sera évaluée pour déterminer le type. La syntaxe est la suivante :

decltype(EXPRESSION) IDENTIFIANT = VALEUR;

À la différence de auto, decltype conserve les modificateurs de type comme const.

main.cpp
int main() {
    const decltype(12) i = 34;  // 12 est une littérale de type 
                                // int, donc decltype déduit le type int.
                                // decltype est précédé de const
                                // donc i est de type const int.
                                // 34 n'est pas utilisé pour évaluer le type de int.
 
    decltype(12) j = 34;        // 12 est une littérale de type 
                                // int, donc decltype déduit le type int
                                // et j est de type int.
 
    decltype(i) k = 34;         // i est une variable de type const int,
                                // donc decltype déduit le type const int
                                // et k est de type const int.
 
    decltype(j) l = 34;         // j est une variable de type int,
                                // donc decltype déduit le type int
                                // et l est de type int.
}

Syntaxes alternatives

Dans le chapitre précédent, vous avez vu la syntaxe avec accolades pour initialiser une variable :

const int i { 123 };
const int j {};

Cette syntaxe n'est pas utilisable directement avec auto (les accolades sont interprétées différemment dans ce cas), ce qui explique que la syntaxe avec affectation = est utilisée. Par contre, les accolades ne posent pas de problème avec decltype, vous pouvez donc utiliser la syntaxe sans problème :

const decltype(123) i { 123 };
const decltype(i) j {};

Pour des raisons d’homogénéité des syntaxes, nous allons utiliser par défaut les accolades pour initialiser une variable sans déduction de type et l'opérateur d'affectation pour l'initialisation de variable avec déduction de type. Mais n'oubliez pas que d'autres syntaxes sont utilisables.

En particulier, pour initialiser une variable avec un type déduit par decltype et une valeur par défaut, on préféra la syntaxe avec accolades :

const decltype(i) j = 0;  // n'a pas de sens si i n'est pas un type entier
const decltype(i) j {};   // syntaxe valide quel que soit le type de i

Erreurs de déduction des types

La déduction de type est une technique intéressante pour avoir un code plus évolutif, mais cela peut produire des surprises si les types déduis par le compilateur ne correspondent pas à ce que le développeur souhaitait. Dans des codes d'exemple simples comme présentés dans ce chapitre, il est facile de ne pas se tromper, mais dans des codes plus complexes, les erreurs arrivent très vite.

S'il fallait donner une règle pour éviter les problèmes, ce serait la suivante :

Toujours s'assurer que le code est suffisamment explicite pour que l'on puisse comprendre les intentions de celui qui écrit le code. Si ce n'est pas le cas, il est préférable de ne pas utiliser la déduction de type.

Prenons quelques contre-exemples, dans lesquels le type de la variable n'est pas forcément explicitement donné par le contexte.

auto j = i;

La règle pour nommer une variable est que le nom doit être explicite, mais certains noms de variables sont tellement communs que leur utilisation ne pose pas de problème de compréhension. C'est en particulier le cas de i, j, k, qui sont utilisés en mathématiques et en programmation comme indices entiers. (La contrepartie est qu'il ne faut pas appeler une variable i si ce n'est pas un indice entier, sinon il y a un risque de confusion).

Donc ici, même si le contexte ne donne pas le type de i, par convention, ce sera un entier et de même pour j.

En cas d'erreur sur les types, dans le meilleur des cas, les opérations réalisées n'auront pas de sens et le compilateur produira un message d'erreur. Imaginons par exemple que vous souhaitez créer une variable entière, mais que vous utilisez une littérale chaîne de caractères :

main.cpp
#include <iostream>
 
int main() {
    const auto i = "hello";            // type entier ?
    std::cout << i * 10 << std::endl;  // erreur
}

Bien sûr, le calcul n'a aucun sens et le compilateur signale qu'il y a un problème dans le code :

main.cpp:5:20: error: invalid operands to binary expression ('const char *' and 'int')
    std::cout << i * 10 << std::endl;  // erreur
                 ~ ^ ~~
1 error generated.

Le message d'erreur n'est pas forcément clair (et un compilateur C++ peut être très doué pour donner des messages d'erreur incompréhensibles). Il indique ici que le compilateur ne trouve pas d'opération entre un type const char* (une chaîne de caractères) et un type int (un entier), pas qu'il y a une erreur sur l'utilisation du mot-clé auto (comment le compilateur pourrait-il le savoir ?)

Mais la situation n'est pas forcément aussi simple. Si le code précédent est modifié pour utiliser l'opérateur +, le résultat est très différent :

main.cpp
#include <iostream>
 
int main() {
    const auto i = "hello";            // type entier ?
    std::cout << i + 10 << std::endl;  // erreur
}

affiche par exemple (cela peut afficher n'importe quoi en fait) :

;0

Pour des raisons historiques, la chaîne est interprétée par le compilateur comme si c'était un entier (attention, ce n'est pas le cas, le type déduit est plus complexe). Aucune erreur n'est détectée. Comme vous l'avez déjà vu, ce code produit un comportement indéterminé (Undefined Behavior), ce qui est souvent très complexe à détecter et corriger. Il convient donc de faire très attention lors de l'utilisation de la déduction de type.

inference_de_type.txt · Dernière modification: 2019/06/28 15:53 par sebastien