04 fév. 2010

const X& x ou X const& x ? (et autres amusements)

Parce que ces deux notations sont strictement équivalentes[1], quelle est celle qu'il faut utiliser ? Est-ce qu'il est nécessaire de préciser que la surcharge d'une méthode virtuelle est elle-même virtuelle ? Faut-il utiliser les version post-fixée ou pré-fixées des opérateurs d"incrémentation/décrémentation ?

Je ne prétends pas vous donner une réponse définitive à ces questions, mais j'ai ma petite idée sur ce qu'il est préférable de faire.

Notes

[1] si vous êtes un peu rouillé sur le sujet et que vous lisez relativement bien l'anglais, vous pouvez vous reporter à la C++ FAQ Lite. Si vous êtes en plus rouillé en anglais, dirigez vous vers la FAQ de dvp.com

const X& x contre X const& x

En C++ comme en C, une déclaration se lit en anglais de la gauche vers la droite - sauf en ce qui concerne const. En cas de doute, rappelez vous que ce mot-clef modifie la mutabilité de l'entité qui le précède. Ainsi, X const* p est un pointeur vers un type X constant (c'est à dire que la valeur de p peut changer, mais pas la variable pointée par p), tandis que X* const p est un pointeur constant vers un type X (c'est à dire que la valeur du pointeur ne peut pas changer, mais la variable pointée par p peut être modifiée). Cette étrange étrangeté rends le code C++ un peu plus compliqué à déchiffrer - comme s'il n'était pas assez compliqué comme ça.

Élargissons un peu le problème. Une variable d'un type pointeur constant sur un type constant se déclare ainsi :

X const* const p;

De cette manière, p n'est pas modifiable (pointeur constant) et la variable pointée par p ne l'est pas non plus (type constant). Cette déclaration est assez lisible, car le mot-clef const est employé plusieurs fois. Qu'on enlève une seule occurrence du mot-clef, et la déclaration devient singulièrement plus complexe :

  • le code est moins clair, parce deux interprétation proches sont possibles si on ne connait pas assez bien son standard C++.
  • il y a une possibilité d'erreur lors de l'écriture du code, puisque l'inversion de * et const modifie le comportement du code de manière drastique.

On peut aussi définir la même variable ainsi :

const X* const p;

Cette définition est fonctionnellement équivalente, mais on la lit autrement - de gauche à droite, pour une fois : il s'agit maintenant d'une variable constante de type pointeur sur un type constant. D'autre part, si on enlève le premier const (et si on prends pour règle de ne jamais écrire X const* à la place de const X*), on réduit l'ambiguïté de la définition du type à la lecture puisqu'on ne confondra pas les deux formes X const* et X* const. Il est donc plus rassurant de définir une variable d'un type constant en préfixant le nom du type avec const plutôt que de le mettre en suffixe.

mais revenons à la question originelle : le problème ne se pose pas en ces termes puisque les références C++ sont nécessairement constantes : X& const n'a donc pas de sens en C++, et il est impossible de le confondre avec X const&. Mais puisqu'on préfère la notation const X* à X const*, il est logique et cohérent de préférer aussi const X& à X const&.

Je mets virtual ou pas?

Soit la classe B :

class B {
public:
  virtual void f() { }
}

Cette classe défini une méthode virtuelle B::f(). Si je crée une classe D qui hérite de B afin de surcharger f(), est-ce qu'il est nécessaire de préciser que f() est virtuelle ou non ?

Bien évidemment, non, ça n'est pas nécessaire : la méthode héritée est déclarée virtuelle implicitement par le compilateur si aucune déclaration explicite n'est présente. Ainsi :

class D : public B {
public:
  void f() { ... }
}

et

class D : public B {
public:
  virtual void f() { ... }
}

Sont des définitions strictement équivalentes. Dans ce cas, est il préférable de préciser explicitement que la méthode est virtuelle ou peut on se contenter du fait que le compilateur l'ajoute de manière implicite ?

Rappelons nous que le code que nous écrivons n'est pas destiné à la machine - mais aux autres programmeurs[1]. La première qualité d'un code source, c'est sa lisibilité. Par définition, une information implicite est une information manquante - elle dégrade la lisibilité. Par conséquent, il est toujours préférable d'être explicite lorsqu'on écrit du code. Ajoutons donc ces 8 caractères - ce n'est pas comme si ça allait provoquer une usure prématurée de votre clavier...

v++ contre ++v

Au moins, celle-la est simple. Les réponses aux autres questions sont largement dépendantes d'un style de programmation - mais la réponse à cette question a des implications techniques définies.

Revenons quand même un peu sur les définitions : l'opérateur postfixé (v++) renvoie la valeur de v, puis incrémente la variable. L'opérateur préfixé ++v) incrémente d'abord la variable puis renvoie la valeur de celle-ci. Dans la plupart des cas, ces opérations d'incrémentation sont effectuées de manière unitaire, et on ne stocker même pas le résultat. Dans d'autres cas, le résultat nous importe. La réponse n°1 est donc triviale : si le résultat de l'opération a une importance pour vous, alors la question ne se pose plus. Soit vous avez besoin de l'opérateur postfixé, soit vous avez besoin de l'opérateur préfixé, mais en aucun cas vous ne pouvez vous permettre de prendre un de ces deux opérateurs au hasard.

Maintenant, il peut arriver que nous n'ayons pas besoin du résultat - seule l'opération d'incrémentation nous intéresse. Par exemple :

for (int i=0; i<size(); i++) {
  ...
}

est strictement équivalent à

for (int i=0; i<size(); ++i) {
  ...
}

(le même code est généré puisque la partie qui récupère la valeur de i est supprimée par le compilateur lors d'une passe d'optimisation).

A priori, les deux possibilités nous sont offertes, donc allons-y gaiment, et laissons la fonction std::rand() décider pour nous de la notation à utiliser. Sauf que (bien évidemment, il y a un "sauf que") l'équivalence stricte que j'ai avancé ci-dessus a une sévère limitation : elle n'est valide que si la variable incrémentée est un type scalaire entier ou sur un pointeur. Hors, le C++ nous permet de redéfinir l'opérateur ++() sur n'importe quelle classe.

L'exemple typique de redéfinition de ++() est l'itérateur du conteneur std::vector<>.

typedef std::vector<type>::iterator itype;
for (itype i=v.begin(); i!=v.end(); ???) { ... }

Doit on remplacer ??? par un incrémentation de i préfixée ou postfixée ? La réponse nous viens de la définition déjà donnée de ces opérateurs, et de la façon dont le compilateur les traite lorsqu'ils ont utilisé.

Le compilateur est intelligent - mais il n'est pas pour autant le plus intelligent de vous deux. Lorsqu'il appelle une fonction, il ne peut pas se permettre de ne pas vider le code de cette fonction à des fins d'optimisation. Ce code peut avoir des effets de bord qu'il ignore, et qui peuvent modifier le comportement du programme. Par conséquent, il met le code complet de la fonction. Si ce code est inliné, il est possible qu'il puisse en éliminer une partie - mais c'est quand même plus difficile que ça en a l'air (ici, internal_state.advance() est l'opération qui effectue l'incrémentation sur l'état interne de l'instance).

Le code typique d'un opérateur ++ préfixé est le suivant :

{
  internal_state.advance();
  return *this;
}

Celui d'un opérateur postfixé correspondant est proche de :

{
  T copy = *this;
  internal_state.advance();
  return copy;
}

L'opérateur postfixé, de part sa définition, a besoin de faire une copie de l'état précédant l'incrémentation - de manière à pouvoir retourner cet état. Sur des objets non triviaux, cette opération peut être coûteuse. Par conséquent, l'opérateur préfixé - qui n'a pas besoin de faire cette copie - est sensiblement plus rapide que la version postfixée.

Lorsque le choix existe, on préfèrera dès lors utilisé la version préfixée de l'opérateur d'incrémentation (ou de décrémentation).

Notes

[1] il pourrait vous arriver des mésaventures du genre "analyse ce code source afin d'y trouver un bug, sachant que tu n'as pas d'accès aux librairies sur lesquelles ce code est basé et que tu ne peux pas le compiler". Avoir tous les outils et toutes les informations nécessaires à la résolution d'un problème est un luxe.

Commentaires

1. Le mardi, février 23 2010, 18:20 par 3DArchi

Salut,
Je savais qu'il y avait un truc qui me gênait dans cette histoire de const. Et en cherchant autre chose, je suis tombé sur cette discussion sur dvp.com : Template abstrait et héritage. Ceci renforce mon sentiment de préférer type const & à const type &.
Sur les opérateurs ++ (ou --) :
Dans la plupart des cas, ces opérations d'incrémentation sont effectuées de manière unitaire, et on ne stocker même pas le résultat. Dans d'autres cas, le résultat nous importe.
Personnellement, lorsque le résultat m'importe, je préfère séparer l'affectation de l'incrément :
res = i;++i;
ou
++i;res = i;
Je trouve que ça lève l'ambigüité. Et c'est d'autant plus critique lorsqu'on voit du :
fonction(++i,i--)
qui a un comportement indéterminé puisqu'on ne sait pas dire quand se font ou ne se font pas les incréments.
Ceci dit, la suite explique bien quand écrire ++ avant ou après.
De même, je ne peux être que 100% d'accord lorsque tu dis que virtual devrait toujours être explicitement indiqué. C'est effectivement une bonne pratique. C++0x devrait permettre de renforcer cette pratique avec les attributs. Exemple de la future norme N3000 (sauf ligne en français) :
@@

class B {
virtual void f(int);
virtual void h(int);
virtual void i(int);
};
class D [[base_check]] : public B {
void f [[override]] (int); // OK: f implicitly virtual, overrides B::f
void h(int); // error: h implicitly virtual, but overriding without marker
virtual void i(int); // devrait être OK
}

@@

2. Le mardi, mars 16 2010, 16:46 par Médinoc

Franchement, pour le coup du const, je suis plutôt l'opinion inverse: Rester cohérent, toujours mettre le const à droite.

Mais pour le virtual, je suis 100% d'accord avec toi. C'est pour ça aussi que j'adore le override du C#, qui est encore plus porteur d'information.

Ajouter un commentaire

Les commentaires peuvent être formatés en utilisant une syntaxe wiki simplifiée.

Fil des commentaires de ce billet