Penser en C++

Volume 1


précédentsommairesuivant

9. Fonctions inlines

Un des aspects importants dont le C++ hérite du C est l'efficacité. Si l'efficacité du C++ était nettement moins grande que celle du C, il y aurait un nombre significatif de programmeurs qui ne pourraient pas justifier son usage.

En C, une des façons de préserver l'efficacité est l'usage de macros, qui vous permet de faire ce qui ressemble à des appels de fonctions sans le temps système normalement associé à ces appels. La macro est implémentée avec le préprocesseur au lieu du compilateur proprement dit, et le préprocesseur remplace toutes les appels de macros directement par le code des macros. Ainsi, il n'y a aucun coût associé à l'empilement d'arguments sur la pile, faire un CALL en langage assembleur, retourner les arguments, et faire un RETURN en langage assembleur. Tout le travail est effectué par le préprocesseur, si bien que vous avez la commodité et la lisibilité d'un appel de fonction mais cela ne vous coûte rien.

Il y a deux problèmes liés à l'usage de macros du préprocesseur en C++. Le premier est aussi vrai en C : une macro ressemble à un appel de fonction, mais ne se comporte pas toujours comme tel. Ceci peut dissimuler des bugs difficiles à trouver. Le deuxième problème est spécifique au C++ : le préprocesseur n'a pas la permission d'accéder aux données membres des classes. Ceci signifie que les macros préprocesseurs ne peuvent pas être utilisées comme fonctions membres d'une classe.

Pour conserver l'efficacité de la macro du préprocesseur, mais y adjoindre la sécurité et la portée de classe des vraies fonctions, C++ dispose des fonctions inlines. Dans ce chapitre, nous examinerons les problèmes des macros du préprocesseur en C++, comment ces problèmes sont résolus avec les fonctions inline, et donneront des conseils et des aperçus sur la façon dont les fontions inline fonctionnent.

9.1. Ecueils du préprocesseurs

La clef du problème des macros du préprocesseur est que vous pouvez être abusivement induits à penser que le comportement du préprocesseur est le même que celui du compilateur. Bien sûr, c'était voulu qu'une macro ressemble et agisse comme un appel de fonction, si bien qu'il est assez facile de tomber dans cette erreur. Les difficultés commencent quand les différences subtiles se manifestent.

Comme exemple simple, considérez le code suivant :

 
Sélectionnez
#define F (x) (x + 1)

A présent, si un appel est fait à F( ) de cette façon :

 
Sélectionnez
F(1)

le préprocesseur le développe, de façon quelque peu inattendue, comme ceci :

 
Sélectionnez
(x) (x + 1)(1)

Le problème se produit à cause de l'espace entre F et sa parenthèse d'ouverture dans la définition de la macro. Quand cet espace est supprimé, vous pouvez de fait appeler la macro avec l'espace

 
Sélectionnez
F (1)

et elle se développera toujours correctement ainsi :

 
Sélectionnez
(1 + 1)

L'exemple ci-dessus est relativement trivial et le problème sera tout de suite apparent. Les vraies difficultés ont lieu quand on utilise des expressions comme argument dans les appels aux macros.

Il y a deux problèmes. Le premier est que les expressions peuvent se développer dans la macro si bien que la priorité de leur évaluation n'est pas celle que vous attendriez. Par exemple :

 
Sélectionnez
#define FLOOR(x,b) x>=b?0:1

A présent, si des expressions sont utilisées en arguments:

 
Sélectionnez
if(FLOOR(a&0x0f,0x07)) // ...

la macro se développera en :

 
Sélectionnez
if(a&0x0f>=0x07?0:1)

La priorité de & est plus basse que celle de >=, si bien que l'évaluation de la macro vous réserve une surprise. Une fois le problème découvert, vous pouvez le résoudre en plaçant des parenthèses autour de tous les éléments dans la définition de la macro. (C'est une bonne habitude quand vous créez des macros de préprocesseur.) Ainsi,

 
Sélectionnez
#define FLOOR(x,b) ((x)>=(b)?0:1)

Découvrir le problème peut être difficile, toutefois, et vous pouvez ne pas le découvrir après avoir estimé que la macro se comporte comme il faut. Dans la version sans parenthèse de la macro précédente, la plupart des expressions se comporteront correctement parce que la priorité de >= est plus basse que celle de la plupart des autres opérateurs comme +, /, - -, et même les opérateurs de manipulation de bits. Ainsi, vous pouvez facilement penser que cela marche avec toutes les expressions, y compris celles qui utilisent des opérateurs logiques de bit.

Le problème précédent peut être résolu par des habitudes de programmation soigneuses : mettre tout entre parenthèses dans une macro. Toutefois, la deuxième difficulté est plus subtile. Contrairement à une fonction normale, à chaque fois que vous utilisez un argument dans une macro, cet argument est évalué. Tant que la macro est appelée seulement avec des variables ordinaires, cette évaluation est sans risque, mais si l'évaluation d'un argument a des effets secondaires, alors le résultat peut être surprenant et ne ressemblera certainement pas au comportement d'une fonction.

Par exemple, cette macro détermine si son argument est compris dans un certain intervalle :

 
Sélectionnez
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)

Tant que vous utilisez un argument "ordinaire", la macro se comporte tout à fait comme une vraie fonction. Mais dès que vous vous laissez aller et commencez à croire que c'est une vraie fonction, les problèmes commencent. Ainsi :

 
Sélectionnez
//: C09:MacroSideEffects.cpp
#include "../require.h"
#include <fstream>
using namespace std;
 
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
 
int main() {
  ofstream out("macro.out");
  assure(out, "macro.out");
  for(int i = 4; i < 11; i++) {
    int a = i;
    out << "a = " << a << endl << '\t';
    out << "BAND(++a)=" << BAND(++a) << endl;
    out << "\t a = " << a << endl;
  }
} ///:~

Remarquez l'utilisation de majuscules dans le nom de la macro. C'est une pratique efficace parce qu'elle signale au lecteur que c'est une macro et pas une fonction, si bien que s'il y a des problèmes, elle agit comme un rappel.

Voici la sortie produite par le programme, qui n'est pas du tout ce à quoi vous vous seriez attendu dans le cas d'une vraie fonction :

 
Sélectionnez
a = 4
  BAND(++a)=0
   a = 5
a = 5
  BAND(++a)=8
   a = 8
a = 6
  BAND(++a)=9
   a = 9
a = 7
  BAND(++a)=10
   a = 10
a = 8
  BAND(++a)=0
   a = 10
a = 9
  BAND(++a)=0
   a = 11
a = 10
  BAND(++a)=0
   a = 12

Quand a vaut quatre, seule la première partie de l'expression conditionnelle a lieu, si bien que l'expression n'est évaluée qu'une fois, et l'effet secondaire de l'appel à la macro est que a passe à cinq, qui est ce que vous attendriez d'un appel à une fonction normale dans la même siutation. Toutefois, quand le nombre est dans l'intervalle, les deux conditions sont testées, ce qui résulte en deux incrémentations. Le résultat est produit en évaluant à nouveau l'argument, ce qui entraîne une troisième incrémentation. Une fois que le nombre sort de l'intervalle, les deux conditions sont toujours testées et vous vous retrouvez toujours avec deux incrémentations. Les effets secondaires diffèrent selon l'argument.

Ce n'est clairement pas le genre de comportement que vous voulez de la part d'une macro qui ressemble à un appel de fonction. Dans ce cas, la solution évidente est d'en faire une vraie fonction, ce qui, bien sûr, ajoute du temps système supplémentaire et peut réduire l'efficacité si vous appelez souvent cette fonction. Malheureusement, le problème peut n'être pas toujours aussi évident, et vous pouvez inconsciemment avoir une librairie qui contienne des fonctions et des macros mélangées ensemble, si bien qu'un problème comme celui-ci peut dissimuler des bugs très difficiles à découvrir. Par exemple, la macro putc( ) dans cstdio peut évaluer son deuxième argument deux fois. C'est une spécification du C Standard. Ainsi, des implémentations peu soigneuses de toupper( ) comme une macro peuvent évaluer l'argument plus d'une fois, ce qui génèrera des résultats inattendus avec toupper(*p++). (45)

9.1.1. Les macros et l'accès

Bien sûr, un codage soigné et l'utilisation des macros du préprocesseur sont nécessaires en C, et nous pourrions certainement échapper à la même chose en C++ si un problème ne se posait pas : une macro n'a aucun concept de portée requis pour les fonctions membres. Le préprocesseur réalise simplement une substitution de texte, si bien que vous ne pouvez pas écrire quelque chose comme :

 
Sélectionnez
class X {
  int i;
public:
#define VAL(X::i) // Erreur

ou même quoi que ce soit qui s'en rapproche. En outre, il n'y aurait aucune indication de l'objet auquel vous feriez référence. Il n'y a tout simplement pas moyen d'exprimer la portée de classe dans une macro. Sans solution alternative aux macros de préprocesseur, les programmeurs seraient tentés de rendre certaines données membres public par souci d'efficacité, exposant ainsi l'implémentation sous-jacente et empêchant des modifications de cette implémentation, et éliminant du même coup la protection fournie par private.

9.2. Fonctions inline

En résolvant le problème en C++ d'une macro ayant accès aux membres private d'une classe, tous les problèmes associés aux macros du préprocesseur ont été éliminés. Ceci a été réalisé en transportant, comme il se doit, le concept de macro sous le contrôle du compilateur. Le C++ implémente la macro comme une fonction inline, qui est une vraie fonction dans tous les sens du terme. Une fonction inline observe tous les comportements que vous attendez d'une fonction ordinaire. La seule différence est qu'une fonction inline est développée sur place, comme une macro du préprocesseur, si bien que le temps système de l'appel à la fonction est éliminé. Ainsi vous ne devriez (presque) jamais utiliser de macros, mais uniquement des fonctions inline.

Toute fonction définie dans le corps d'une classe est automatiquement inline, mais vous pouvez aussi rendre inline une fonction qui n'appartient pas à une classe en la faisant précéder du mot-clef inline. Toutefois, pour qu'il ait un effet, vous devez inclure le corps de la fonction à la déclaration, autrement le compilateur la traitera comme une déclaration de fonction ordinaire. Ainsi,

 
Sélectionnez
inline int plusOne(int x);

n'a aucun autre effet que de déclarer la fonction (qui peut avoir ou ne pas avoir une définition inline plus loin). L'approche efficace fournit le corps de la fonction :

 
Sélectionnez
inline int plusOne(int x) { return ++x; }

Remarquez que le compilateur vérifiera (comme il le fait toujours) l'usage correct de la liste des arguments de la fonction et de la valeur de retour (réalisant toute conversion nécessaire), ce dont est incapable le préprocesseur. Egalement, si vous essayez d'écrire le code ci-dessus comme une macro du préprocesseur, vous obtenez un effet secondaire non désiré.

Vous aurez presque toujours intérêt à placer les définitions inline dans un fichier d'en-tête. Quand le compilateur voit une telle fonction, il place le type de la fonction (la signature combinée avec la valeur de retour) et le corps de la fonction dans sa table de symboles. Quand vous utilisez la fonction, le compilateur s'assure que l'appel est correct et que la valeur de retour est utilisée correctement, et substitue ensuite le corps de la fonction à l'appel, éliminant ainsi le temps système. Le code inline occupe réellement de la place, mais si la fonction est petite, il peut prendre moins de place que le code généré par un appel de fonction ordinaire (placer les arguments sur la pile et effectuer l'appel).

Une fonction inline dans un fichier d'en-tête a un statut spécial, puisque vous devez inclure le fichier d'en-tête contenant la fonction et sa définition dans tous les fichiers où la fonction est utilisée, mais vous ne vous retrouvez pas avec des erreurs de déclarations mutliples (toutefois, la déclaration doit être identique partout où la fonction inline est incluse).

9.2.1. Les inline dans les classes

Pour définir une fonction inline, vous devez normalement faire précéder la définition de la fonction du mot-clef inline. Toutefois, ce n'est pas nécessaire dans une définition de classe. Toute fonction que vous définissez dans une définition de classe est automatiquement inline. Par exemple :

 
Sélectionnez
//: C09:Inline.cpp
// Inlines inside classes
#include <iostream>
#include <string>
using namespace std;
 
class Point {
  int i, j, k;
public:
  Point(): i(0), j(0), k(0) {}
  Point(int ii, int jj, int kk)
    : i(ii), j(jj), k(kk) {}
  void print(const string& msg = "") const {
    if(msg.size() != 0) cout << msg << endl;
    cout << "i = " << i << ", "
         << "j = " << j << ", "
         << "k = " << k << endl;
  }
};
 
int main() {
  Point p, q(1,2,3);
  p.print("valeur de p");
  q.print("valeur de q");
} ///:~

Ici, les deux constructeurs et la fonction print( ) sont toutes inline par défaut. Remarquez dans le main( ) que le fait que vous utilisez des fonctions inline est transparent, comme il se doit. Le comportement logique d'une fonction doit être identique indépendamment du fait qu'elle est inline (sinon, c'est votre compilateur qui est en cause). La seule différence que vous constaterez est la performance.

Bien sûr, la tentation est d'utiliser des fonctions inline partout dans les définitions de classe parce qu'elle vous éviteront l'étape supplémentaire d'écrire la définition externe de la fonction membre. Gardez à l'esprit, toutefois, que le but d'inline est de procurer au compilateur de meilleures opportunités d'optimisation. Mais rendre inline une grosse fonction dupliquera ce code partout où la fonction est appelée produisant une inflation du code qui peut réduire le bénéfice de rapidité (le seul procédé fiable est de faire des expériences pour évaluer les effets de inline sur votre programme avec votre compilateur).

9.2.2. Fonctions d'accès

Une des utilisations les plus importantes de inline dans les classe est la fonction d'accès. C'est une petite fonction qui vous permet de lire ou de modifier des parties de l'état d'un objet - c'est-à-dire, une variable interne ou des variables. L'exemple suivant vous montre pourquoi inline est si important pour les fonctions d'accès :

 
Sélectionnez
//: C09:Access.cpp
// Fonctions d'accès inline
 
class Access {
  int i;
public:
  int read() const { return i; }
  void set(int ii) { i = ii; }
};
 
int main() {
  Access A;
  A.set(100);
  int x = A.read();
} ///:~

Ici, l'utilisateur de la classe n'est jamais en contact direct avec les variables d'état dans la classe, et elle peuvent être private, sous le contrôle du concepteur de la classe. Tous les accès aux données membres private peuvent être controlés grâce à l'interface de la fontion membre. En outre, l'accès est remarquablement efficace. Prenez le read( ), par exemple. Sans les inline, le code généré pour l'appel à read( ) impliquerait typiquement de placer this sur la pile et réaliser un appel de fonction en assembleur. Pour la plupart des machines, la taille de ce code serait plus grande que celle du code créé par inline, et le temps d'exécution serait certainement plus grand.

Sans les fonctions inline, un concepteur de classe attentif à l'efficacité sera tenté de faire de i un membre publique, éliminant le temps système en autorisant l'utilisateur à y accéder directement. Du point de vue conception, c'est désastreux parce que i fait ainsi partie de l'interface publique, ce qui signifie que le concepteur de la classe ne pourra jamais le changer. Vous êtes coincés avec un int appelé i. C'est un problème parce que vous pourriez apprendre plus tard qu'il serait bien plus utile de représenter l'information d'état par un float plutôt que par un int, mais puisque int i fait partie de l'interface publique, vous ne pouvez le modifier. Ou bien vous pouvez vouloir réaliser des calculs supplémentaires en même temps que vous lisez ou affectez i, ce que vous ne pouvez pas faire s'il est public. Si, par contre, vous avez toujours utilisé des fonctions membres pour lire et changer l'état de l'information d'un objet, vous pouvez modifier la représentation sous-jacente de l'objet autant que vous le désirez.

En outre, l'utilisation de fonctions membres pour controler les données membres vous permet d'ajouter du code à la fontion membre pour détecter quand les données sont modifiées, ce qui peut être très utile pour déboguer. Si une donnée membre est public, n'importe qui peut la modifier sans que vous le sachiez.

Accesseurs et muteurs

Certaines personnes distinguent encore dans le concept de fonction d'accès les accesseurs(pour lire l'état de l'information d'un objet) et les muteurs(pour changer l'état d'un objet). De plus, la surcharge de fonction peut être utilisée pour donner le même nom à l'accesseur et au muteur ; la façon dont vous appelez la fonction détermine si vous lisez ou modifiez l'état de l'information. Ainsi,

 
Sélectionnez
//: C09:Rectangle.cpp
// Accesseurs & muteurs
 
class Rectangle {
  int wide, high;
public:
  Rectangle(int w = 0, int h = 0)
    : wide(w), high(h) {}
  int width() const { return wide; } // lit
  void width(int w) { wide = w; } // assigne
  int height() const { return high; } // lit
  void height(int h) { high = h; } // assigne
};
 
int main() {
  Rectangle r(19, 47);
  // Modifie width & height:
  r.height(2 * r.width());
  r.width(2 * r.height());
} ///:~

Le constructeur utilise la liste d'initialisation du constructeur (brièvement introduite au Chapitre 8 et couverte en détails au Chapitre 14) pour initialiser les valeurs de wide et high(en utilisant la forme pseudoconstructeur pour type prédéfinis).

Vous ne pouvez avoir de noms de fonctions membres utilisant le même identifiant que les données membres, si bien que vous pouvez être tentés de différencier les données membres à l'aide d'un underscore en premier caractère. Toutefois, les identifiants avec underscore sont réservés et vous ne devez pas les utiliser.

Vous pourriez choisir, à la place, d'utiliser “get” et “set” pour indiquer accesseurs et muteurs :

 
Sélectionnez
//: C09:Rectangle2.cpp
// Accesseurs & muteurs avec "get" et "set"
 
class Rectangle {
  int width, height;
public:
  Rectangle(int w = 0, int h = 0)
    : width(w), height(h) {}
  int getWidth() const { return width; }
  void setWidth(int w) { width = w; }
  int getHeight() const { return height; }
  void setHeight(int h) { height = h; }
};
 
int main() {
  Rectangle r(19, 47);
  // Modifie width & height:
  r.setHeight(2 * r.getWidth());
  r.setWidth(2 * r.getHeight());
} ///:~

Bien sûr, accesseurs et muteurs ne sont pas nécessairement de simple tuyaux vers les variables internes. Parfois ils peuvent réaliser des calculs plus élaborés. L'exemple suivant utilise la libraire de fonction standard du C time pour produire une classe Time simple :

 
Sélectionnez
//: C09:Cpptime.h
// Une classe time simple 
#ifndef CPPTIME_H
#define CPPTIME_H
#include <ctime>
#include <cstring>
 
class Time {
  std::time_t t;
  std::tm local;
  char asciiRep[26];
  unsigned char lflag, aflag;
  void updateLocal() {
    if(!lflag) {
      local = *std::localtime(&t);
      lflag++;
    }
  }
  void updateAscii() {
    if(!aflag) {
      updateLocal();
      std::strcpy(asciiRep,std::asctime(&local));
      aflag++;
    }
  }
public:
  Time() { mark(); }
  void mark() {
    lflag = aflag = 0;
    std::time(&t);
  }
  const char* ascii() {
    updateAscii();
    return asciiRep;
  }
  // Différence en secondes:
  int delta(Time* dt) const {
    return int(std::difftime(t, dt->t));
  }
  int daylightSavings() {
    updateLocal();
    return local.tm_isdst;
  }
  int dayOfYear() { // Depuis le 1er janvier
    updateLocal();
    return local.tm_yday;
  }
  int dayOfWeek() { // Depuis dimanche
    updateLocal();
    return local.tm_wday;
  }
  int since1900() { // Année depuis 1900
    updateLocal();
    return local.tm_year;
  }
  int month() { // Depuis janvier
    updateLocal();
    return local.tm_mon;
  }
  int dayOfMonth() {
    updateLocal();
    return local.tm_mday;
  }
  int hour() { // Depuis minuit, 24 h pile
    updateLocal();
    return local.tm_hour;
  }
  int minute() {
    updateLocal();
    return local.tm_min;
  }
  int second() {
    updateLocal();
    return local.tm_sec;
  }
};
#endif // CPPTIME_H ///:~

Les fonctions de librairie standard du C ont plusieurs représentations du temps, et elles font toutes partie de la classe Time. Toutefois, il n'est pas nécessaire de les mettre toutes à jour, si bien qu'à la place time_t t est utilisée comme représentation de base, et tm local et la représentation en caractères ASCII asciiRep ont chacune des indicateurs pour indiquer si elles ont été mises à jour à l'heure courante time_t ou non. Les deux fonctions privateupdateLocal( ) et updateAscii( ) vérifient les indicateurs et effectuent une mise à jour conditionnelle.

Le constructeur appelle la fonction mark( )(que l'utilisateur peut également appeler pour forcer l'objet à représenter le temps courant), et cela vide les indicateurs pour signaler que le temps local et la représentation ASCII sont maintenant invalides. La fonction ascii( ) appelle updateAscii( ), qui copie le résultat de la fonction de la librairie C standard asctime( ) dans un buffer local car asctime( ) utilise une zone de donnée statique qui est écrasée si la fonction est appelée ailleurs. La valeur de retour de la fonction ascii( ) est l'adresse de ce buffer local.

Toutes les fonctions commençant avec daylightSavings( ) utilisent la fonction updateLocal( ), qui rend les inline composites qui en résultent relativement grands. Cela ne semble pas intéressant, surtout en considérant que vous n'appelerez sans doute pas souvent ces fonctions. Cependant, cela ne signifie pas que toutes les fonctions doivent être rendues non inline. Si vous rendez d'autres fonctions non inline, conservez au moins updateLocal( ) inline afin que son code soit dupliqué dans les fonctions non inline, éliminant des temps systèmes d'appel supplémentaires.

Voici un petit programme test :

 
Sélectionnez
//: C09:Cpptime.cpp
// Tester une classe time simple
#include "Cpptime.h"
#include <iostream>
using namespace std;
 
int main() {
  Time start;
  for(int i = 1; i < 1000; i++) {
    cout << i << ' ';
    if(i==0) cout << end1;
  }
 
  Time end;
  cout << endl;
  cout << "debut = " << start.ascii();
  cout << "fin = " << end.ascii();
  cout << "delta = " << end.delta(&start);
} ///:~

Un objet Time est créé, puis une activité consommant du temps est effectuée, puis un second objet Time est créé pour noter le temps de fin. On les utilise pour donner les temps de début, de fin et le temps écoulé.

9.3. Stash & Stack avec les inlines

Armé des inlines, nous pouvons maintenant convertir les classes Stash et Stack pour être plus efficaces :

 
Sélectionnez
//: C09:Stash4.h
// Inline functions
#ifndef STASH4_H
#define STASH4_H
#include "../require.h"
 
class Stash {
  int size;      // Taille de chaque espace mémoire
  int quantity;  // Nombre d'espaces de stockage
  int next;      // Espace libre suivant
  // Tableau d'octets alloué dynamiquement:
  unsigned char* storage;
  void inflate(int increase);
public:
  Stash(int sz) : size(sz), quantity(0),
    next(0), storage(0) {}
  Stash(int sz, int initQuantity) : size(sz), 
    quantity(0), next(0), storage(0) { 
    inflate(initQuantity); 
  }
  Stash::~Stash() {
    if(storage != 0) 
      delete []storage;
  }
  int add(void* element);
  void* fetch(int index) const {
    require(0 <= index, "Stash::fetch (-)index");
    if(index >= next)
      return 0; // Pour indiquer la fin
    // Produit un pointeur vers l'élément désiré:
    return &(storage[index * size]);
  }
  int count() const { return next; }
};
#endif // STASH4_H ///:~

Les petites fonctions fonctionnent bien de toute évidence comme inline, mais remarquez que les deux plus grandes fonctions ne sont pas transformées en inline, étant donné que cela n'entraînerait probablement aucun gain de performance:

 
Sélectionnez
//: C09:Stash4.cpp {O}
#include "Stash4.h"
#include <iostream>
#include <cassert>
using namespace std;
const int increment = 100;
 
int Stash::add(void* element) {
  if(next >= quantity) // Assez d'espace libre ?
    inflate(increment);
  // Copie l'élément dans l'espace de stockage,
  // à partir du prochain espace libre:
  int startBytes = next * size;
  unsigned char* e = (unsigned char*)element;
  for(int i = 0; i < size; i++)
    storage[startBytes + i] = e[i];
  next++;
  return(next - 1); // Index
}
 
void Stash::inflate(int increase) {
  assert(increase >= 0);
  if(increase == 0) return;
  int newQuantity = quantity + increase;
  int newBytes = newQuantity * size;
  int oldBytes = quantity * size;
  unsigned char* b = new unsigned char[newBytes];
  for(int i = 0; i < oldBytes; i++)
    b[i] = storage[i]; // Copie l'ancien en nouveau
  delete [](storage); // Libère l'ancien espace de stockage
  storage = b; // Pointe vers la nouvelle mémoire
  quantity = newQuantity; // Ajuste la taille
} ///:~

Une fois de plus le programme de test vérifie que tout fonctionne correctement:

 
Sélectionnez
//: C09:Stash4Test.cpp
//{L} Stash4
#include "Stash4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
 
int main() {
  Stash intStash(sizeof(int));
  for(int i = 0; i < 100; i++)
    intStash.add(&i);
  for(int j = 0; j < intStash.count(); j++)
    cout << "intStash.fetch(" << j << ") = "
         << *(int*)intStash.fetch(j)
         << endl;
  const int bufsize = 80;
  Stash stringStash(sizeof(char) * bufsize, 100);
  ifstream in("Stash4Test.cpp");
  assure(in, "Stash4Test.cpp");
  string line;
  while(getline(in, line))
    stringStash.add((char*)line.c_str());
  int k = 0;
  char* cp;
  while((cp = (char*)stringStash.fetch(k++))!=0)
    cout << "stringStash.fetch(" << k << ") = "
         << cp << endl;
} ///:~

C'est le même programme de test que nous avons utilisé auparavant, donc la sortie devrait être fondamentalement la même.

La classe Stack fait même un meilleur usage des inline:

 
Sélectionnez
//: C09:Stack4.h
// With inlines
#ifndef STACK4_H
#define STACK4_H
#include "../require.h"
 
class Stack {
  struct Link {
    void* data;
    Link* next;
    Link(void* dat, Link* nxt): 
      data(dat), next(nxt) {}
  }* head;
public:
  Stack() : head(0) {}
  ~Stack() {
    require(head == 0, "Stack pas vide");
  }
  void push(void* dat) {
    head = new Link(dat, head);
  }
  void* peek() const { 
    return head ? head->data : 0;
  }
  void* pop() {
    if(head == 0) return 0;
    void* result = head->data;
    Link* oldHead = head;
    head = head->next;
    delete oldHead;
    return result;
  }
};
#endif // STACK4_H ///:~

Remarquez que le destructeur de Link qui était présent mais vide dans la version précédente de Stack a été supprimé. Dans pop( ), l'expression delete oldHead libère simplement la mémoire utilisée par Link(elle ne détruit pas l'objet data pointé par le Link).

La plupart des fonctions peuvent être rendues inline aisément, en particulier pour Link. Même pop( ) semble légitime, bien qu'à chaque fois que vous avez des variables conditionnelles ou locales il n'est pas évident que inline soit tellement profitable. Ici, la fonction est suffisamment petite pour que cela ne gêne probablement pas.

Si toutes vos fonctions sont inline, utiliser la librairie devient relativement simple parce qu'il n'y a pas besoin d'une édition de liens, comme vous pouvez le constater dans l'exemple test (remarquez qu'il n'y a pas de Stack4.cpp):

 
Sélectionnez
//: C09:Stack4Test.cpp
//{T} Stack4Test.cpp
#include "Stack4.h"
#include "../require.h"
#include <fstream>
#include <iostream>
#include <string>
using namespace std;
 
int main(int argc, char* argv[]) {
  requireArgs(argc, 1); // Le nom de fichier est l'argument
  ifstream in(argv[1]);
  assure(in, argv[1]);
  Stack textlines;
  string line;
  // Lire le fichier et stocker les lignes dans la pile :
  while(getline(in, line))
    textlines.push(new string(line));
  // Dépiler les lignes et les afficher:
  string* s;
  while((s = (string*)textlines.pop()) != 0) {
    cout << *s << endl;
    delete s; 
  }
} ///:~

Les gens écriront parfois des classes avec toutes les fonctions inline si bien que la classe sera entièrement dans le fichier d'en-tête (vous verrez dans ce livre que j'ai moi-même franchi cette limite). Pendant le développement c'est probablement sans conséquences, bien que cela puisse rendre la compilation plus longue. Une fois que le programme se stabilise un peu, vous aurez intérêt à revenir en arrière et rendre non inline les fonctions appropriées.

9.4. Les inline & le compilateur

Pour comprendre quand la déclaration inline est efficace, il est utile de savoir ce que le compilateur fait quand il rencontre un inline. Comme avec n'importe quelle fonction, le compilateur détient le type de la fonction (c'est-à-dire, le prototype de la fonction incluant le nom et le type des arguments, combinés à la valeur de retour de la fontion) dans sa table des symboles. En outre, quand le compilateur voit que le type de la fonction inline et le corps de la fonction s'analysent sans erreur, le code du corps de la fonction est également amené dans la table des symboles. Que ce code soit stocké sous forme source, d'instructions assembleur compilées, ou tout aute représentation est du ressort du compilateur.

Quand vous faites un appel à une fonction inline, le compilateur s'assure tout d'abord que l'appel peut être correctement effectué. C'est-à-dire que soit les types des arguments passés doivent correspondre exactement aux types de la liste des arguments de la fonction, soit le compilateur doit être capable de faire une conversion vers le type correct, et la valeur de retour doit être du bon type (ou une conversion vers le bon type) dans l'expression de destination. Ce processus, bien sûr, est exactement ce que fait le compilateur pour n'importe quelle fonction et est nettement différent de ce que le préprocesseur réalise parce que le préprocesseur ne peut pas vérifier les types ou faire des conversions.

Si toutes les informations de type de la fonction s'adaptent bien au contexte de l'appel, alors le code inline est substitué directement à l'appel de la fonction, éliminant le surcoût de temps de l'appel et permettant davantage d'optimisations par le compilateur. Egalement, si le inline est une fonction membre, l'adresse de l'objet ( this) est placé aux endroit appropriés, ce qui constitue bien sûr une autre opération dont le préprocesseur est incapable.

9.4.1. Limitations

Il y a deux situations dans lesquelles le processeur ne peut pas réaliser l'opération inline. Dans ces cas, il revient simplement à la forme ordinaire d'une fonction en prenant la définition de la fonction inline et en créant l'espace de stockage pour la fonction exactement comme il le fait pour une fonction non inline. S'il doit le faire dans plusieurs unités de traduction (ce qui causerait normalement une erreur pour définitions multiples), l'éditeur de liens reçoit l'instruction d'ignorer les définitions multiples.

Le compilateur ne peut réaliser l'inline si la fonction est trop complexe. Ceci dépend du compilateur lui-même, mais au degré de complexité auquel la plupart des compilateurs abandonnent, le inline ne vous ferait gagner probablement aucune efficacité. En général, tout type de boucle est considéré trop complexe pour être développé en inline, et si vous y réfléchissez, les boucles nécessitent probablement beaucoup plus de temps dans la fonction que ce qui est requis en temps système par l'appel à la fonction. Si la fonction est simplement une collection d'instructions, le processeur n'aura probablement aucune difficulté à réaliser le inline, mais s'il y a beaucoup d'instructions, le temps système d'appel de la fonction sera beaucoup plus court que celui de l'exécution du corps de la fonction. Et rappelez-vous, à chaque fois que vous appelez une grosse fonction inline, tout le corps de la fonction est inséré à l'emplacement de chacun des appels, si bien que vous pouvez facilement obtenir un alourdissement du code sans amélioration notable des performances. (Remarquez que certains exemples dans ce livre peuvent excéder des tailles d'inline raisonnables afin de sauver de l'espace sur l'écran.)

Le compilateur ne peut pas non plus réaliser l'inline si l'adresse de la fonction est prise implicitement ou explicitement. Si le compilateur doit fournir une adresse, alors il allouera un espace de stockage pour le code de la fonction et utilisera l'adresse qui y correspond. Toutefois, là où une adresse n'est pas requise, le compilateur réalisera probablement toujours l'inline du code.

Il est important de comprendre qu'un inline est seulement une suggestion au compilateur ; le compilateur n'est pas obligé de rendre inline quoi que ce soit. Un bon compilateur rendra inline les fonctions petites et simples et ignorera intelligemment les inline trop complexes. Ceci produira le résultat que vous recherchez - la sémantique d'un appel de fonction avec l'efficacité d'une macro.

9.4.2. Déclarations aval

Si vous imaginez ce que fait le compilateur pour implémenter les inline, vous risquez de vous tromper en imaginant qu'il y a plus de limitations qu'il n'en existe réellement. En particulier, si un inline fait une déclaration aval vers une fonction qui n'a pas encore été déclarée dans la classe (que cette fonction soit inline ou non) il peut sembler que le compilateur ne sera pas capable de gérer le situation :

 
Sélectionnez
//: C09:EvaluationOrder.cpp
// Ordre d'évaluation de inline
 
class Forward {
  int i;
public:
  Forward() : i(0) {}
  // Appel à une fonction non déclarée:
  int f() const { return g() + 1; }
  int g() const { return i; }
};
 
int main() {
  Forward frwd;
  frwd.f();
} ///:~

Dans f( ), un appel est fait à g( ), bien que g( ) n'ait pas encore été déclarée. Ceci fonctionne parce que la définition du langage déclare qu'aucune fonction inline dans une classe ne sera évaluée avant l'accolade de fermeture de la déclaration de classe.

Bien sûr, si g( ) à son tour appelait f( ), vous finiriez avec un ensemble d'appels récursifs, ce qui serait trop complexe pour que le compilateur puisse réaliser l'inline. (Vous auriez également à réaliser des tests dans f( ) ou g( ) pour forcer l'une des deux à terminer ce qui risquerait d'être, autrement, une récursion infinie.)

9.4.3. Activités cachées dans les constructeurs et les destructeurs

Les constructeurs et les destructeurs sont deux endroits ou vous pouvez être abusivement amenés à penser qu'un inline est plus efficace qu'il ne l'est réellement. Les constructeurs et les destructeurs peuvent avoir des activités cachées, si la classe contient des sous objets dont les constructeurs et les destructeurs doivent être appelés. Ces sous-objets peuvent être des objets membres, ou bien ils peuvent exister à cause de l'héritage (couvert au Chapitre 14). Voici un exemple de classe avec objets membres:

 
Sélectionnez
//: C09:Hidden.cpp
// Activités cachées dans les inline
#include <iostream>
using namespace std;
 
class Member {
  int i, j, k;
public:
  Member(int x = 0) : i(x), j(x), k(x) {}
  ~Member() { cout << "~Member" << endl; }
};
 
class WithMembers {
  Member q, r, s; // Ont des constructeurs
  int i;
public:
  WithMembers(int ii) : i(ii) {} // Trivial?
  ~WithMembers() {
    cout << "~WithMembers" << endl;
  }
};
 
int main() {
  WithMembers wm(1);
} ///:~

Le constructeur pour Member est suffisamment simple pour être inline, puisqu'il n'y a de spécial à réaliser - par d'héritage ou d'objets membres qui causeraient des activités supplémentaires dissimulées. Mais dans class WithMembers, il y a plus en jeu que ce que l'on voit au premier abord. Les constructeurs et les destructeurs pour les objets membres q, r, et s sont appelés automatiquement, et ces constructeurs et destructeurs sont aussi inline, ce qui constitue une différence significative d'avec les fonctions membres ordinaires. Ceci ne signifie pas que vous devez systématiquement vous abstenir de déclarer inline les destructeurs et les constructeurs ; il y a des situations ou c'est intéressant. En outre, quand vous "esquissez" un programme en écrivant rapidement du code, il est souvent plus pratique d'utiliser des inline. Mais is vous êtes intéressés par l'efficacité, c'est quelque chose à quoi il faut prêter attention.

9.5. Réduire le fouillis

Dans un livre comme celui-ci, la simplicité et la concision des définitions de fonctions inline dans les classes est très utile parce qu'on peut en mettre davantage sur une page ou un écran (pour un séminaire). Toutefois, Dan Saks (46)a souligné que dans un projet réel cela a l'effet de rendre l'interface de la classe inutilement fouillis et ainsi de la rendre plus difficile à utiliser. Il fait référence aux fonctions membres définies dans les classes par l'expression latine in situ(sur place) et affirme que toutes les définitions devraient être maintenues en dehors de la classe pour garder l'interface propre. Il soutient que l'optimisation est un problème séparé. Si vous voulez optimiser, utilisez le mot-clef inline. En appliquant cette approche, Rectangle.cpp vu précédemment devient:

 
Sélectionnez
//: C09:Noinsitu.cpp
// Supprimer les fonctions in situ
 
class Rectangle {
  int width, height;
public:
  Rectangle(int w = 0, int h = 0);
  int getWidth() const;
  void setWidth(int w);
  int getHeight() const;
  void setHeight(int h);
};
 
inline Rectangle::Rectangle(int w, int h)
  : width(w), height(h) {}
 
inline int Rectangle::getWidth() const {
  return width;
}
 
inline void Rectangle::setWidth(int w) {
  width = w;
}
 
inline int Rectangle::getHeight() const {
  return height;
}
 
inline void Rectangle::setHeight(int h) {
  height = h;
}
 
int main() {
  Rectangle r(19, 47);
  // Transpose width & height:
  int iHeight = r.getHeight();
  r.setHeight(r.getWidth());
  r.setWidth(iHeight);
} ///:~

A présent, si vous voulez comparer les effets des fonctions inline aux fonctions non inline, vous pouvez simplement supprimer le mot-clef inline. (Toutefois les fonctions inline devraient normalement être placées dans des fichiers d'en-tête tandis que les fonctions non inline devraient être situées dans leur propre unité de traduction.) Si vous voulez mettre les fonctions dans la documentation, une simple opération de couper-coller suffit. Les fonctions in situ requièrent plus de travail et présentent plus de risques d'erreurs. Un autre argument en faveur de cette approche est que vous pouvez toujours produire un style de formatage cohérent pour les définitions de fonctions, ce qui n'est pas toujours le cas avec les fonctions in situ.

9.6. Caractéristiques supplémentaires du préprocesseur

Un peu plus tôt, j'ai dit que vous avez presque toujours intérêt à utiliser des fonctions inline à la place des macros du préprocesseur. Les exceptions à cette règle sont quand vous avez besoin de trois caractéristiques spéciales du préprocesseur C (qui est également le préprocesseur du C++) : le chaînage, la concaténation de chaîne et le collage de jetons. Le chaînage, déjà introduit dans ce livre, est réalisé avec la directive # et vous permet de prendre un identifiant et de le transformer en tableau de caractères. La concaténation de chaîne a lieu quand deux tableaux de caractères adjacents n'ont pas d'intervalle de ponctuation, auquel cas ils sont combinés. Ces deux caractéristiques sont particulièrement utiles quand on écrit du code pour débugger. Ainsi,

 
Sélectionnez
#define DEBUG(x) cout << #x " = " << x << endl

Ceci affiche la valeur de n'importe quelle variable. Vous pouvez aussi obtenir une trace qui imprime les instructions au fur et à mesure de leur exécution :

 
Sélectionnez
#define TRACE(s) cerr << #s << endl; s

Le #s transforme l'instruction en tableau de caractères pour la sortie, et le deuxième s réitère l'instruction afin qu'elle soit exécutée. Bien sûr, ce genre de chose peut poser des problèmes, particulièrement dans des boucles for d'une ligne:

 
Sélectionnez
for(int i = 0; i < 100; i++)
 TRACE(f(i));

Comme il y a en fait deux instructions dans la macro TRACE( ), la boucle for d'une ligne exécute seulement la première. La solution est de remplacer le point-virgule par une virgule dans la macro.

9.6.1. Collage de jeton

Le collage de jeton, implémenté avec la directive ##, est très utile quand vous créez du code. Il vous permet de prendre deux identifiants et de les coller ensemble pour créer automatiquement un nouvel identifiant. Par exemple,

 
Sélectionnez
#define FIELD(a) char* a##_string; int a##_size
class Record {
  FIELD(one);
  FIELD(two);
  FIELD(three);
  // ...
}; 

Chaque appel à la macro FIELD( ) crée un identifiant pour contenir un tableau de caractères et un deuxième pour contenir la longeur de ce tableau. Non seulement est-ce plus simple à lire, mais cela élimine des erreurs de codage et rend la maintenance plus facile.

9.7. Vérification d'erreurs améliorée

Les fonctions de require.h ont été utilisées jusque là sans les définir (bien que assert( ) ait été également utilisée pour aider à la détection des erreurs du programmeur quand approprié). Il est temps, maintenant, de définir ce fichier d'en-tête. Les fonctions inlines sont commodes ici parce qu'elles permettent de tout mettre dans un fichier d'en-tête, ce qui simplifie l'utilisation du package. Vous incluez simplement le fichier d'en-tête et vous n'avez pas à vous en faire à propos de l'édition des liens et du fichier d'implémentation.

Vous devriez noter que les exceptions (présentées en détail dans le deuxième volume de cet ouvrage) procure une manière bien plus efficace de gérer beaucoup de types d'erreurs - spécialement celles dont vous voudriez pouvoir récupérer - plutôt que de simplement arrêter le programme. Les conditions que gère require.h, toutefois, empêchent le programme de se poursuivre, par exemple si l'utilisateur ne fournit pas pas suffisamment d'arguments dans la ligne de commande ou si un fichier ne peut pas être ouvert. Ainsi, il est raisonnable qu'elles appellent la fonction de la librairie C standard exit( ).

Le fichier d'en-tête suivant se trouve dans le répertoire racine du livre et on peut donc y accéder facilement depuis tous les chapitres.

 
Sélectionnez
//: :require.h
// Teste les conditions d'erreur dans les programmes
// "using namespace std" local pour les vieux compilateurs
#ifndef REQUIRE_H
#define REQUIRE_H
#include <cstdio>
#include <cstdlib>
#include <fstream>
#include <string>
 
inline void require(bool requirement, 
  const std::string& msg = "Requirement failed"){
  using namespace std;
  if (!requirement) {
    fputs(msg.c_str(), stderr);
    fputs("\n", stderr);
    exit(1);
  }
}
 
inline void requireArgs(int argc, int args, 
  const std::string& msg = 
    "Must use %d arguments") {
  using namespace std;
   if (argc != args + 1) {
     fprintf(stderr, msg.c_str(), args);
     fputs("\n", stderr);
     exit(1);
   }
}
 
inline void requireMinArgs(int argc, int minArgs,
  const std::string& msg =
    "Must use at least %d arguments") {
  using namespace std;
  if(argc < minArgs + 1) {
    fprintf(stderr, msg.c_str(), minArgs);
    fputs("\n", stderr);
    exit(1);
  }
}
 
inline void assure(std::ifstream& in, 
  const std::string& filename = "") {
  using namespace std;
  if(!in) {
    fprintf(stderr, "Could not open file %s\n",
      filename.c_str());
    exit(1);
  }
}
 
inline void assure(std::ofstream& out, 
  const std::string& filename = "") {
  using namespace std;
  if(!out) {
    fprintf(stderr, "Could not open file %s\n", 
      filename.c_str());
    exit(1);
  }
}
#endif // REQUIRE_H ///:~

Les valeurs par défaut fournissent des messages raisonnables, qui peuvent être modifiés si besoin est.

Vous remarquerez qu'au lieu d'utiliser des arguments char*, on utilise des arguments const string&. Ceci permet l'utilisation de char* et de string comme arguments de ces fonctions, et est donc utilisable plus largement (vous pouvez avoir intérêt à adopter cette forme dans votre propre code).

Dans les définitions de requireArgs( ) et requireMinArgs( ), le nombre d'arguments nécessaires sur la ligne de commande est augmenté de un parce que argc inclue toujours le nom du programme en cours d'exécution comme argument zéro, et a donc toujours une taille supérieure d'un argument au nombre d'arguments passés en ligne de commande.

Remarquez l'utilisation locale de “ using namespace std” dans chaque fonction. Ceci est dû au fait que certains compilateurs au moment de la rédaction de cet ouvrage n'incluaient pas - improprement - la bibliothèque de fonctions standards du C dans namespace std, si bien qu'une qualification explicite causerait une erreur à la compilation. Les déclarations locales permettent à require.h de travailler avec les deux types de biliothèques, correctes et incorrectes, sans ouvrir l'espace de nommage std à la place de quiconque incluant ce fichier en-tête.

Voici un programme simple pour tester require.h:

 
Sélectionnez
//: C09:ErrTest.cpp
//{T} ErrTest.cpp
// Teste require.h
#include "../require.h"
#include <fstream>
using namespace std;
 
int main(int argc, char* argv[]) {
  int i = 1;
  require(i, "value must be nonzero");
  requireArgs(argc, 1);
  requireMinArgs(argc, 1);
  ifstream in(argv[1]);
  assure(in, argv[1]); // Utilise le nom de fichier
  ifstream nofile("nofile.xxx");
  // Echoue :
//!  assure(nofile); // L'argument par défaut
  ofstream out("tmp.txt");
  assure(out);
} ///:~

Vous pourriez être tenté de faire un pas de plus pour l'ouverture de fichiers et d'ajouter une macro à require.h:

 
Sélectionnez
#define IFOPEN(VAR, NAME) \
  ifstream VAR(NAME); \
  assure(VAR, NAME);

Qui pourrait alors être utilisée comme ceci :

 
Sélectionnez
IFOPEN(in, argv[1])

Au premier abord, cela peut paraître intéressant puisque cela signifie qu'il y a moins de choses à taper. Ce n'est pas trop dangereux, mais c'est un chemin sur lequel il vaut mieux éviter de s'engager. Remarquez que, encore une fois, une macro ressemble à une fonction mais se comporte différemment ; elle crée en fait un objet ( in) dont la portée persiste au-delà de la macro. Peut-être comprenez-vous la situation, mais pour de nouveaux programmeurs et pour ceux qui maintiennent le code, cela constitue une énigme supplémentaire à résoudre. Le C++ est suffisamment compliqué pour qu'il n'y ait pas besoin d'ajouter à la confusion. Essayez donc de vous convaincre vous-même de ne pas utiliser les macros du préprocesseur si elles ne sont pas indispensables.

9.8. Résumé

Il est critique que vous soyez capables de cacher l'implémentation sous-jacente d'une classe parce que vous pourriez vouloir modifier cette implémentation ultérieurement. Vous effectuerez ces changements pour des raisons d'efficacité, ou parce que vous aurez une meilleure compréhension du problème, ou parce qu'une nouvelle classe est devenue disponible et que vous voulez l'utiliser dans l'implémentation. Tout élément qui porte atteinte au caractère privé de l'implémentation sous-jacente réduit la flexibilité du langage. Ainsi, la fonction inline et très importante parce qu'elle élimine virtuellement le besoin de macros du préprocesseur et les problèmes qui y sont liés. Avec les inlines, les fonctions membres peuvent être aussi efficaces que les macros du préprocesseur.

On peut abuser de la fonction inline dans les définitions de classes, bien sûr. Le programmeur est tenté de le faire parce que c'est plus facile, et donc cela se produira. Toutefois, ce n'est pas un si gros problème par la suite, quand vous cherchez à réduire la taille, car vous pouvez toujours rendre les fonctions non inline sans effet sur leur fonctionnalité. La ligne conductrice de la programmation devrait être : “faites le fonctionner d'abord, optimisez-le ensuite”.

9.9. Exercices

Les solutions à certains exercices peuvent être trouvées dans le document électronique The Thinking in C++ Annotated Solution Guide, disponible pour une somme modique sur www.BruceEckel.com.

  1. Ecrivez un programme qui utilise la macro F( ) montrée au début du chapitre et montrez qu'il ne se développe pas proprement, comme décrit dans le texte. Réparez la macro et montrez qu'elle fonctionne correctement.
  2. Ecrivez un programme qui utilise la macro FLOOR( ) montrée au début du chapitre. Montrez les conditions sous lesquelles il ne fonctionne pas proprement.
  3. Modifiez MacroSideEffects.cpp afin que BAND( ) fonctionne bien.
  4. Créez deux fonctions identiques, f1( ) et f2( ). Rendez f1( ) inline, mais pas f2( ). Utilisez la fonction clock( ) de la bibliothèque standard du C qui se trouve dans <ctime> pour noter les moments de début et de fin et comparez les deux fonctions pour voir laquelle est la plus rapide. Vous pourriez avoir besoin de faire plusieurs appels aux fonctions au sein de vos boucles de chronométrage pour obtenir des valeurs significatives.
  5. Faites varier la complexité du code dans les fonctions de l'exercice 4 pour voir si vous pouvez trouver le point d'équilibre où les deux fonctions prennent le même temps. Si vous en disposez, essayez de le faire avec différents compilateurs et notez les différences.
  6. Montrez quelles fonctions inline font appel par défaut à la liaison interne.
  7. Créez une classe qui contienne un tableau de char. Ajoutez un constructeur inline qui utilise la fonction memset( ) de la bibliothèque standard du C pour initialiser le tableau à l'argument du constructeur (par défaut, faites en sorte que ce soit ‘ '), ajoutez une fonction membre inline appelée print( ) pour afficher tous les caractères du tableau.
  8. Prenez l'exemple NestFriend.cpp du chapitre 5 et remplacez toutes les fonctions membres avec des inline. Faites des fonctions inline qui ne soient pas in-situ. Convertissez également les fonctions initialize( ) en constructeurs.
  9. Modifiez StringStack.cpp du Chapitre 8 pour utiliser des fonctions inline.
  10. Créez un enum appelé Hue contenant red, blue, et yellow. A présent, créez une classe appelée Color contenant une donnée membre de type Hue et un constructeur qui affecte le Hue à la valeur de son argument. Ajoutez des fonctions d'accès “get” et “set” pour lire et fixer le Hue. Rendez toutes ces fonctions inline.
  11. Modifiez l'exercice 10 pour utiliser l'approche “accesseur” et “muteur”.
  12. Modifiez Cpptime.cpp de façon à ce qu'il mesure le temps qui sépare le démarrage du programme de l'instant où l'utilisateur tape la touche “Entrée”.
  13. Créez une classe avec deux fonctions inline, de façon à ce que la première fonction qui est définie dans la classe appelle la deuxième, sans qu'il y ait besoin d'une déclaration anticipée. Ecrivez un main qui crée un objet de la classe et appelle la première fonction.
  14. Créez une classe A avec un constructeur par défaut inline qui s'annonce lui-même. Puis créez une nouvelle classe, B et placez un objet A comme membre de B, et donnez à B un constructeur inline. Créez un tableau d'objets B et voyez ce qu'il se produit.
  15. Créez un grand nombre d'objets de l'exercice précédent et utilisez la classe Time pour chronométrer la différence entre constructeurs inline ou non. (Si vous disposez d'un profiler, essayez de l'utiliser également.)
  16. Ecrivez un programme qui prend une string comme argument en ligne de commande. Ecrivez une boucle for qui supprime un caractère de la string à chaque passage, et utilisez la macro DEBUG( ) de ce chapitre pour imprimer la string à chaque fois.
  17. Corrigez la macro TRACE( ) comme spécifié dans ce chapitre, et prouvez qu'elle focntionne correctement.
  18. Modifiez la macro FIELD( ) afin qu'elle contienne également un nombre index. Créez une classe dont les membres sont composés d'appels à la macro FIELD( ). Ajoutez une fonction membre qui vous permette de consulter un champ en utilisant son numéro d'index. Ecrivez un main( ) pour tester la classe.
  19. Modifiez la macro FIELD( ) afin qu'elle génère automatiquement des fonctions d'accès pour chaque champ (les données devraient toutefois toujours être privées). Créez une classe dont les fonctions membres sont composées d'appels à la macro FIELD( ). Ecrivez un main( ) pour tester la classe.
  20. Ecrivez un programme qui prenne deux arguments en ligne de commande : le premier est un int et le second un nom de fichier. Utilisez require.h pour garantir que vous avez le bon nombre d'arguments, que le int est compris entre 5 et 10, et que le fichier peut être ouvert avec succès.
  21. Ecrivez un programme qui utilise la macro IFOPEN( ) pour ouvrir un fichier comme input stream. Notez la création de l'objet ifstream et sa portée.
  22. (Difficile) Trouvez comment faire générer du code assembleur par votre compilateur. Créez un fichier contenant une très petite fonction et un main( ) qui appelle la fonction. Générez le code assembleur quand la fonction est inline ou ne l'est pas, et montrez que la version inline n'a pas le surcoût de temps associé à l'appel.

précédentsommairesuivant
Andrew Koenig entre davantage dans les détails dans son livre C Traps & Pitfalls(Addison-Wesley, 1989) traduit en français Les pièges du C(Addison-Wesley france, 1992).
Co-auteur avec Tom Plum de C++ Programming Guidelines, Plum Hall, 1991.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2005 Bruce Eckel. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.