Arrivés à ce stade, nous avons déjà vu l'essentiel de ce qui touche à la const-correctness. Normalement vous devriez pouvoir écrire un code const-correct qui fera l'admiration de vos proches (ne rêvons pas trop non plus). Pourtant nous n'avons pas fini de parler de ce détail que vous avez pu trouver parfaitement anodin à vos débuts.
Utilisez const autant que possible, mais pas trop.
Comme toutes les bonnes choses (à part le Logiciel Libre évidemment

), il ne faut pas abuser de
const !
Voici un exemple relativement fréquent d'un emploi absolument inutile de ce mot-clé :
Code : C++ | void maFonction(const int);
|
Même si nous n'allons pas modifier le paramètre dans la fonction, l'utilisateur n'en a que faire, puisque vous ne récupérez qu'une copie. Dans ce cas, vous avez pris la peine d'écrire 5 caractères de trop, qui alourdissent votre prototype et n'apportent rien sémantiquement.
En bref, pour les types fondamentaux contentez-vous d'une copie non constante (si vous avez pensé « il manque la référence » c'est qu'il faut que vous relisiez la note à la fin du paragraphe sur le passage par référence constante

).
Autre emploi contestable de const :
Code : C++ | class Exemple
{
public :
const int get() const {return my_int;}
private :
int my_int;
};
|
Bien entendu la fonction
doit être constante, seul le retour est concerné. Ici l'intérêt du
const est également nul. Le retour n'est pas une référence ni un pointeur, donc l'utilisateur ne pourrait de toutes façons pas modifier l'état de l'objet (mon compilateur a même la gentillesse de me prévenir que le qualificateur n'a aucun effet dans ce cas).
La situation est presque identique dans le cas d'objets, à ceci près qu'un retour constant permet d'éviter l'absurdité sémantique suivante :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14 | class Exemple
{
public :
std::string get() const {return my_str;}
private :
std::string my_str;
};
int main()
{
Exemple e;
e.get() = "gné ?";
}
|
Ce code compile sans problème. A vous de juger si cela vaut la peine d'ajouter un
const.
Dernier cas, peut-être plus problématique.
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14 | class Exemple
{
public :
const std::string& get() const {return my_str;}
private :
std::string my_str;
};
//Ou encore
const Exemple& foo(const Exemple& e)
{
//Du code
return e;
}
|
Je vous ai promis de ne pas rentrer dans des détails d'optimisation, alors pour faire simple : renvoyez une copie, personne ne vous en voudra. De toutes façons le compilateur optimise le retour la majeure partie du temps, alors n'alourdissez pas le prototype de vos fonctions inutilement.
Un cas dangereux (et donc à bannir) :
Code : C++ | const Exemple& foo()
{
return Exemple();
}
|
Ici votre compilateur devrait vous avertir : vous renvoyez une référence constante sur un objet qui est détruit à la fin de la portée (oui, ce n'est pas la variable créée par l'instruction Exemple() qui est renvoyée, seulement une référence dessus). Autrement dit vous allez manipuler un objet qui n'existe plus. Je vous laisse imaginer les conséquences.
Constance et typage
Vous l'avez peut-être deviné, je vous le confirme : utiliser
const modifie le type de vos variables.
Plus clairement, pour un type
T donné,
const T est d'un type différent.
Mais quelque chose devrait alors vous interpeler. Reprenons un des premiers exemples de cet article :
Code : C++ | int i = 0, j = 1;
int const * p = &i;
*p = j;
p = &j;
|
Ici nous faisons pointer notre pointeur sur un
int constant sur un
int... non constant.
C'est une opération que vous avez sans doute fait sans vous apercevoir, mais si le typage du C++ était plus fort, cette opération ne serait pas acceptée.
En C++ le transtypage d'un pointeur sur un type non constant à un pointeur sur un type constant est implicite, ce qui veut dire que vous n'avez pas à vous en préoccuper. En revanche l'opération inverse n'est pas permise ! Il est impossible d'initialiser directement un pointeur ou une référence sur un type non constant avec un pointeur sur un type constant. Dans le cas inverse vous pourriez modifier l'objet constant comme s'il ne l'était pas.
Dans ce cas, pourquoi peut-on le faire lorsqu'il s'agit de types non composés ? Tout simplement parce qu'une copie est créée dans ce cas. En réalité peu importe que la variable à copier soit ou non constante, elle n'est qu'un modèle et ne sera pas modifiée. Elle peut donc être systématiquement considérée comme étant constante, ce qui est le type le plus restrictif.
Une question pour vous : quelle est la condition pour que soit correcte la surcharge d'une fonction ?
La réponse est : que les types de paramètres soient différents.
On pourrait donc s'attendre à pouvoir surcharger une fonction attendant une variable d'un type T non constant avec une version attendant un T constant. Et bien non. En fait, la surcharge n'est possible
que dans le cas de types composés (pointeurs, références, tableaux...).
Code : C++ | void foo(const std::string&){std::cout << "Je prends une référence constante en paramètre" << std::endl;}
void foo(std::string&) {std::cout << "Je prends une référence non constante en paramètre" << std::endl;}
//Essayez de retirer les références : si ça compile, changez de compilateur.
int main()
{
std::string ex = "";
foo("ex");
foo(ex);
}
|
Comment le compilateur procède-t-il pour déterminer quelle fonction sera appelée ? La règle est simple : si cela est possible, la version de la fonction prenant en paramètre une référence non constante sera appelée. Dans le cas contraire c'est la version prenant une référence constante qui sera appelée.
Dans notre cas :
Code : Console | Je prends une référence constante en paramètre
Je prends une référence non constante en paramètre |
Notez qu'il n'est pas possible de jouer avec la constance des pointeurs, seulement avec le type pointé. Autrement dit, dans le cas d'une surcharge, un pointeur constant sur T (n'importe quel type) sera considéré équivalent à un pointeur non constant sur T.
Autre conséquence de cette différence de types : une erreur peut survenir lorsque vous utilisez des
classes templates. Par exemple, un test d'égalité de types statique (qui vérifie à la compilation si deux types sont égaux, si si ça peut avoir de l'intérêt) échouerait si les paramètres passés étaient
int et
const int.
Je vous le dit parce que cela m'est déjà arrivé mais c'est assez rare, rassurez-vous.
Si jamais vous ne trouvez pas l'origine d'une erreur de compilation en utilisant une classe template, vérifiez la const-correctness de votre code.
En revanche, toujours avec les templates, il y a une erreur dont vous n'avez pas à vous soucier, c'est l'accumulation de
const :
Code : C++ | template<typename T>
void foo(const T);
int main()
{
const int i;
foo(i);
}
|
Comme nous l'avons dit précédemment, nous sommes autorisés à accumuler autant de
const que nous voulons sur un seul type, le résultat sera toujours le même : un type constant.
Dernière petite chose, en manipulant des paramètres templates vous pouvez recevoir des classes comme des types fondamentaux.
Quid alors d'un passage de paramètres dont le type dépends d'un template ?
Choisirez-vous le passage par référence constante ou par copie ?
En règle général mon conseil est de choisir la référence constante. Vous n'êtes jamais à l'abri de l'utilisation d'une classe extrêmement lourde, et le passage d'un
bool par référence constante sera toujours moins dommageable que la copie d'un
std::array<int, 10000>.
Si vous désirez quand même faire un choix plus précis, sachez qu'une bibliothèque de boost (
call_traits) le fait pour vous, tout en réglant quelques autres menus problèmes. Mais souvenez-vous de ce que Donald Knuth vous dirait : « Early optimization is the root of much evil ».
Quelques détails sur les objets constants
Revenons si vous le voulez bien sur le cas des objets constants. Il y a quelques petits détails que je voudrais aborder. Ceux-ci n'ont pas une grande importance mais tant qu'à faire essayons d'être exhaustifs sur le sujet.
Comme vous le savez, un objet dont la classe ne définit aucun constructeur est si possible construit à l'aide d'un constructeur trivial par défaut lorsqu'on l'instancie.
Ceci n'est pas le cas pour un objet constant. Code : C++ | struct Exemple
{
};
int main()
{
const Exemple e; //Erreur ! Exemple n'a pas de constructeur
Exemple f; //Ok, appel au constructeur trivial
}
|
La raison à cela est qu'
une variable constante doit obligatoirement être initialisée à la construction. Or, dans ce cas-là, l'appel au constructeur par défaut n'est pas une initialisation. D'ailleurs, si vous rajoutez des attributs à notre classe Exemple, vous verrez que le constructeur par défaut ne les initialise pas.
Pour conclure, retenez qu'il faut toujours initialiser explicitement une valeur constante, à moins qu'il ne s'agisse d'une instance d'une classe pour laquelle est défini un constructeur par défaut.
Les objets constants doivent donc nécessairement être initialisés à la construction. Cela vaut aussi pour les attributs constants, et la seule manière que nous avons d'initialiser un sous-objet à sa construction, c'est dans la liste d'initialisation de l'objet englobant :
Code : C++ | struct Exemple
{
const int i;
Exemple(int x) : i(x) //Seul moyen d'initialiser un attribut constant
{
}
};
|
Petite exception : les attributs statiques constants. Comme il est impossible de passer par la liste d'initialisation, il va falloir recourir à une autre syntaxe.
Code : C++ | struct Exemple
{
static const int mon_attribut_statique;
};
const int Exemple::mon_attribut_statique = 0;
|
Vous connaissez sans doute cette syntaxe, elle est identique à celle permettant d'initialiser un attribut statique non constant. Seulement cette fois vous ne pouvez pas vous en passer.
Deuxième point à aborder : vous vous souvenez qu'un objet constant étend sa constance à tous ses sous-objets. Ceci ne vaut pas pour les objets pointés par les pointeurs ou références membres. Un exemple sera sans doute plus parlant :
Code : C++ | struct Exemple
{
int* ptr; //Cela vaut aussi pour une référence
void foo() const {*ptr = 0;} //OK...
};
|
Cela est un peu contre-intuitif, je vous l'accorde, mais pas complètement absurde. Le but d'un pointeur est en effet de référencer une autre variable. Cette dernière n'appartient donc pas à l'objet qui contient le pointeur, et la constance ne s'étend pas jusqu'à elle.
Dernière chose, bien utile pour les conteneurs : il est possible de surcharger une fonction membre en proposant une version constante et une version non constante prenant des paramètres de même type ! Voyons plutôt :
Code : C++ 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 | #include <string>
#include <iostream>
class Exemple
{
public :
int get() const
{
return mon_attribut;
}
int& get() //Je vous rappelle que le type de retour n'est pas discriminant pour déterminer si la surcharge est possible.
{
return mon_attribut;
}
private :
int mon_attribut;
};
void afficher(const Exemple& e)
{
std::cout << e.get() << std::endl;
// Notez que dans cette fonction, la ligne suivante ne compilerait pas, bien que la variable renvoyée par get soit une copie :
// e.get() = 1; // Erreur
//
// En revanche, comme indiqué précédement, si get renvoyait un std::string, ceci compilerait
// e.get() = "gné"; // Ok
//
// Toutefois, e.mon_attribut ne serait pas modifié.
}
int main()
{
Exemple exemple; //Mon objet n'est pas initialisé souvenez-vous. mon_attribut peut potentiellement avoir n'importe quelle valeur.
exemple.get() = 1; //Syntaxe étrange, mais remplacez get par l'opérateur d'indexation (operator[])... Ça y est, vous y êtes ?
afficher(exemple);
}
|
En pratique, je vous déconseille fortement ce type de code (renvoyer une référence sur un attribut) qui brise complètement l'encapsulation de votre classe. Notez comme pour la surcharge des paramètres que le compilateur appellera en priorité la version non constante de la fonction.
Je pense que vous saisissez l'intérêt de cette surcharge : vous pouvez ainsi proposer des services différents selon que l'objet sur lequel cette fonction sera appelé est ou non constant. Exemple : un accès en lecture dans le cas d'un conteneur constant (c'est donc quelque chose qu'il faudra systématiquement faire dans ce cas).