• Aucun résultat trouvé

Compléments divers

Dans le document Mieux programmer c++ (Page 196-200)

Quelle est la différence entre ces quatre lignes de code ?

T t;

T t();

T t(u);

T t = u;

(T désigne une classe)

Ces lignes illustrent trois différentes formes d’initialisation possibles pour un objet : initialisation par défaut, initialisation directe et initialisation par le constructeur de copie. Quant à la quatrième forme, il s’agit d’un petit piège à éviter !

Prenons les lignes dans l’ordre :

T t;

Il s’agit ici d’une initialisation par défaut : cette ligne déclare une variable de type

T, nommée t, initialisée par le constructeur par défaut T::T().

T t();

Voici le piège ! Même si elle ressemble à une initialisation de variable, cette ligne n’initialise rien du tout : elle est interprétée par le compilateur comme la déclaration

P

B N

° 42. I

NITIALISATION DE VARIABLES

D

IFFICULTÉ

: 3

Maîtrisez-vous toujours parfaitement ce que vous écrivez ? Pour le vérifier, examinez les quatre lignes de code proposées ici : leur syntaxe est très proche, mais elles ont toutes un comporte-ment différent.

S

OLUTION

d’une fonction nommée t, ne prenant aucun paramètre et renvoyant un objet de type T

(si cela ne vous semble pas évident, remplacez le type T par int et le nom t par f : cela donne « int f(); » ce qui est clairement une déclaration de fonction).

D'aucuns suggèrent qu’il est possible d’utiliser la syntaxe « auto T t(); » pour bien spécifier au compilateur qu’on souhaite déclarer et initialiser un objet automati-que t de type T, et non pas une fonction nommée t renvoyant un T. Ce n’est pas une pratique recommandable, pour deux raisons. La première est que cela ne marchera qu’avec certains compilateurs – au passage non conformes à la norme C++, un compi-lateur correctement implémenté devant rejeter cette ligne en indiquant qu’il n’est pas possible de spécifier le qualificatif ‘auto’ pour un type de retour de fonction. La seconde est qu’il y a une technique beaucoup plus simple pour obtenir le même résul-tat : écrire « T t ; ».Ne cherchez pas la complication lorsque vous écrivez un pro-gramme ! La maintenance de votre code n’en sera que plus facile.

T t(u);

Il s’agit ici de l’initialisation directe. L’objet t est initialisé dès sa construction à partir de la valeur de la variable u, par appel du constructeur T::T(u).

T t = u;

Il s’agit, pour finir, de l’initialisation par le constructeur de copie. L’objet t est initialisé par le constructeur de copie de T, lui-même éventuellement précédé par l’appel d’une autre fonction.

Cette dernière initialisation fonctionne de la manière suivante :

Si u est de type T, cette instruction est équivalente à « T::t(u) » (le constructeur de copie de T est appelé directement).

Si u n’est pas de type T, cette instruction est équivalente à « T t(T(u)); » (u est converti en un objet temporaire de type T, lui-même passé en paramètre au constructeur de copie de T). Il faut savoir qu’en fonction du niveau d’optimisation demandé, certains compilateurs pourront éventuellement supprimer cet appel au constructeur de copie pour le remplacer par une initialisation directe du type « T t(u) ». Il ne faut donc pas que la cohérence de votre code repose sur l’hypothèse que le constructeur de copie sera systématiquement appelé dans une initialisation de ce type.

Erreur courante

Il ne faut pas confondre initialisation par le constructeur de copie et affectation. En dépit de la présence d’un signe =, l’instruction « T t=u; » n’appelle pas T::operator=().

Recommandation

Préférez, lorsque cela est possible, l’emploi d’une initialisation de type « T t(u) » au lieu de

« T t = u; ». Ces deux instructions sont fonctionnellement équivalentes, mais la première pré-sente plus d’avantages – comme, en particulier, la possibilité de prendre plusieurs paramètres.

:

Examinez le programme ci-dessus. Sans en changer la structure – légèrement condensée à des fins de clarté – commentez l’utilisation des mots-clés const.

Proposez une version corrigée du programme à laquelle vous aurez ajouté ou ôté des const (et effectué les éventuels changements mineurs corrélatifs)

Question supplémentaire : y a t’il, dans le programme original, des instructions pouvant provoquer des erreurs à l’exécution en raison de const oubliés ou, au contraire, superflus ?

vector<Point>::iterator i;

for( i = points_.begin(); i != points_.end(); ++i ) area_ += /* calcul de l’aire (non détaillé ici) */;

}

vector<Point> points_;

double area_;

};

Polygon operator+( Polygon& lhs, Polygon& rhs ) {

Polygon ret = lhs;

int last = rhs.GetNumPoints();

for( int i = 0; i < last; ++i ) // concaténation des points {

ret.AddPoint( rhs.GetPoint(i) );

P

B N

° 43. D

U BON USAGE DE

const D

IFFICULTÉ

: 6

const est un outil puissant, pouvant contribuer nettement à la stabilité et à la sécurité d’un programme. Il faut néanmoins être judicieux dans son utilisation. Ce problème présente quel-ques cas où const doit être évité ou au contraire, utilisé.

}

return ret;

}

void f( const Polygon& poly ) {

const_cast<Polygon&>(poly).AddPoint( Point(0,0) );

}

void g( Polygon& const rPoly ) { rPoly.AddPoint( Point(1,1) ); } void h( Polygon* const pPoly ) { pPoly->AddPoint( Point(2,2) ); } int main()

{

Polygon poly;

const Polygon cpoly;

f(poly);

f(cpoly);

g(poly);

h(&poly);

}

Ce problème est l’occasion de signaler, d’une part, des erreurs courantes dans l’utilisation de const, et également, d’autre part, des situations plus subtiles pouvant requérir – ou non – l’utilisation de const (voir notamment le paragraphe « const et

mutable : des amis qui vous veulent du bien »)

class Polygon {

public:

Polygon() : area_(-1) {}

void AddPoint( const Point pt ) { InvalidateArea();

points_.push_back(pt); } 1. L’objet Point étant passé par valeur, il n’y a aucun intérêt à le déclarer const, étant donné que la fonction n’aura, de toute façon, aucune possibilité de modifier l’objet original.

Signalons au passage que deux fonctions ayant la même signature et ne différant que par les attributs const des paramètres passés par valeur sont considérées comme une seule et même fonction par le compilateur (pas de surcharge) :

int f( int );

int f( const int ); // re-déclaration f(int)

// Pas de surcharge, une seule fonction f int g( int& );

int g( const int& ); // Surcharge

// Pas la même fonction que g(int&)

S

OLUTION

Point GetPoint( const int i ) { return points_[i]; }

2. Même commentaire. Il est inutile de déclarer const un paramètre passé par valeur.

3. En revanche, la fonction GetPoint() devrait être déclarée const car elle ne modi-fie pas l’état de l’objet.

4. GetPoint() devrait plutôt retourner un « const Point », afin d’éviter que le code appelant ne modifie l’objet temporaire renvoyé lors de l’exécution de la fonction.

C’est une remarque générale s’appliquant à toutes les fonctions renvoyant un paramè-tre par valeur (sauf lorsqu’il s’agit d’un type prédéfini comme int ou long)1.

Au passage, on pourrait se demander pourquoi GetPoint() ne renvoie pas une référence plutôt qu’une valeur, permettant ainsi aux appelants de placer la valeur retournée à gauche d’un opérateur d’affectation (comme, par exemple, dans l’instruc-tion : « poly.GetPoint(i) = Point(2,2); »). Certes, ce type d’écriture est pratique, mais l’idéal est tout de même de renvoyer une valeur constante ou encore mieux, une référence constante, comme nous allons le voir plus loin, notamment au moment de l’étude de la fonction operator+().

int GetNumPoints() { return points_.size(); }

5. Même commentaire qu’au point (3) : cette fonction devrait être déclarée const car elle ne modifie pas l’état de l’objet.

double GetArea() {

if( area_ < 0 ) // Si l’aire n’a pas été calculée...

{

CalcArea(); // ...on la calcule }

return area_;

}

Recommandation

Il n’est pas nécessaire de spécifier l’attribut const au niveau de la déclaration de la fonc-tion pour un paramètre passé par valeur. En revanche, si ce paramètre n’est pas modifié par la fonction, spécifiez l’attribut const au niveau de la définition.

1. Il n’y a aucun intérêt à renvoyer une valeur constante pour un type prédéfini, au contraire. Le fait q’une fonction renvoit un « const int » plutôt qu’un « int » est non seulement inutile (un type de retour de type prédéfini ne peut de toute façon pas être placé à gauche d’une affectation), mais en plus dangereux (cela peut gêner l’instancia-tion des modèles de classe ou de foncl’instancia-tion). À ce sujet, voir aussi Lakos 96 (p. 618), auteur à propos duquel il faut d’ailleurs signaler qu’il n’est pas, contrairement à moi, favorable à l’emploi de valeur de retour const même pour les types objets.

Recommandation

Les fonctions retournant un objet par valeur doivent en général renvoyer une valeur constante (sauf s’il s’agit d’un type prédéfini, comme int ou long).

Dans le document Mieux programmer c++ (Page 196-200)