Glossaire de g++ (g++ - partie 9)

Cet article léger (pour faire suite à l'article sur les exceptions) présente un regroupement de définitions liées au C++. De plus, nous en avons grosso modo fini avec les généralités du c++ et cet article est une bonne occasion de se les remémorer avant d'attaquer des sujets plus pratiques. Enfin, cet article me permet d'introduire des sujets pour lesquels je n'ai pas écrit d'article.

Petit glossaire du C++

Il est des mots clés et des termes liés au C++ dont on oublie parfois le sens. Cet article rappelle la signification de quelques uns de ces mots. Lorsque c'est intéressant les définitions sont agrémentées d'exemples.

Instance (LMAG n°46)

Une classe définit un type d'objet, et un objet est une instance de classe. On peut faire le parallèle avec l'Humain en tant que classe et une naissance comme étant une instantiation.

Encapsulation (LMAG n°46)

Le procédé d'encapsulation consiste à définir des "droits d'accès" aux méthodes et propriétés d'une classe.

Les droits d'accès peuvent être principalement de trois types, public, protected et private.

Les membres publics sont accessibles par n'importe quelle partie ou entité du programme.
Les membres protégés sont accessibles par les instances de la classe qui les définit et par les instances des classes qui en héritent.
les membres privés ne sont accessibles que par les instances de la classe qui les définit.

Héritage (multiple) (LMAG n°47 & 49)

En C++, quand on parle d'héritage, il s'agit de définir une classe à partir d'une autre. Par exemple, imaginons une classe bouton qui nous convienne presque. On peut créer une classe boutonbip qui hérite de bouton et se comporte donc de la même façon si ce n'est qu'on lui fait faire un bip, lors d'un click. Une telle classe ressemblerait à :

class boutonbip : public bouton {
    void beep(); // implémentation sans intérêt ici
public:
    boutonbip() : bouton() {};
    onclick() {beep(); bouton::onclick();};
}

La classe boutonbip hérite de bouton. Dans la méthode onclick on ajoute le bip et on appelle la méthode onclick de la classe bouton.

En c++ une classe peut hériter de plusieurs autres mais ça peut vite devenir une source d'erreur. Une possibilité à utiliser avec parcimonie donc. D'autres langages orientés objet ne proposent pas cette fonctionnalité comme par exemple le Java. Le C++ vous laisse le choix de l'utiliser ou non.

La définition d'une classe héritant de plusieurs autres commence ainsi :

class boutonbip : public bouton, public beep { ...

Dans les exemples ci-dessus l'héritage est public, mais il peut également être protégé ou privé, comme nous l'avons déjà évoqué dans un article précédent.

Surcharge (LMAG n°45)

Surcharger revient à déclarer et définir une fonction portant le même nom qu'une fonction existante mais prenant d'autres paramètres ou des paramètres de types différents que l'originale.

On peut surcharger une fonction globale ou une méthode d'une classe parente.

void do_it(int& i) { i /= 2; }
void do_it(int& i, int n) { i /= n; }

Dans ce cas on peut appeler la fonction avec un ou deux paramètres. Selon le cas, l'une ou l'autre version sera utilisée, ainsi, do_it(5) renverra 2 et do_it(9, 3) renverra 3

Notons que dans ce cas, on aurait pu remplacer ces deux déclarations en utilisant des paramètres par défaut, comme ceci :
void do_it(int& i, int n=2) { i /= n; }

Polymorphisme (LMAG n°49)

Le vrai polymorphisme est intimement lié à la notion de fonction virtuelle (voir plus bas, le mot clé virtual).
Prenons un exemple :

#include <iostream>
using namespace std;

class A {
public:
    void do_it() { cout << "I'm an A" << endl; }
};
class B : public A {
public:
    void do_it() { cout << "I'm a B" << endl; }
};

class C {
public:
    virtual void do_it() { cout << "I'm a C" << endl; }
};
class D : public C {
public:
    virtual void do_it() { cout << "I'm a D" << endl; }
};


int main(void) {
    A a;
    B b;
    A* obj;
    obj = &a;
    obj->do_it();
    obj = &b;
    obj->do_it();

    C c;
    D d;
    C* vobj;
    vobj = &c;
    vobj->do_it();
    vobj = &d;
    vobj->do_it();
    return 0;
}
L'exécution donne :
$ ./polymorphisme
I'm an A
I'm an A
I'm a C
I'm a D

Si on n'utilise pas de fonction virtuelle, la méthode do_it() appelée à partir d'un pointeur de type A* est toujours celle de A, même lorsque l'objet pointé est de type B. Notons qu'on peut tout à fait pointer un objet de type B avec un pointeur A* puisque B hérite de A.

Dans le cas où on utilise des méthodes virtuelles, ce n'est plus le type du pointeur qui compte mais le type de l'objet pointé. C'est ce que l'on appelle le polymorphisme..

Patrons (LMAG n°45)

Les patrons, modèles ou template permettent de déclarer et définir des méthodes ou classes dépendant de certains paramètres mais aussi de leur types.

Je m'explique, lorsque vous déclarez une fonction vous pouvez lui passer un certain nombre de paramètres, lesquels doivent être d'un type prédéfini (ce n'est pas tout à fait vrai dans l'absolu, cf. le cas de printf(char*, ...)). Les patrons permettent d'avoir un paramètre de type non connu à l'avance, par exemple :

#include <iostream>
using namespace std;

template<class tClasse> tClasse do_it (tClasse param) {
    return param/2;
}

int main(void) {
    cout << do_it(8) << endl;
    cout << do_it(3.2) << endl;
    return 0;
}

C'est un des nombreux mécanismes mis à disposition du développeur c++ pour la réutilisabilité du code.

STL (LMAG n°44)

C'est le nom de la librairie standard du C++. STL signifie Standard Template Library. Comme son nom l'indique, c'est une source idéale d'inspiration pour créer des templates compliquées... Vous y trouvez notamment toutes les définitions des conteneurs standards du C++ (vector, map, queue, ...) et des classes de bases (exceptions, chaînes de caractères, etc...).

Classes abstraites (LMAG n°46)

Une classe abstraite est une classe contenant au moins une méthode virtuelle pure. C'est à dire une méthode virtuelle déclarée mais non définie. Pour rappel, on termine la déclaration d'une méthode virtuelle pure par =0 pour bien montrer ce qu'elles sont.

Ces classes servent à modéliser des interfaces. C'est aux classes qui héritent de ces interfaces de définir les méthodes virtuelles pures.

Design patterns

Les design patterns sont des solutions à des problèmes courants regroupées sous ce terme un peu abstrait. On y trouve par exemple les singletons (problème: comment être sûr que je n'aurai qu'une seule instance de ma classe). Une littérature abondante est présente sur internet sur ce sujet et je n'écarte pas l'idée de faire un article là-dessus mais ce ne sera définitivement pas au sein de ce glossaire.

RTTI

Autrement dit "RunTime Type Information". Ce mécanisme permet de connaître le type d'un objet pendant l'exécution du programme.

Pour l'utiliser, on doit inclure l'entête typeinfo. Ensuite, on appelle typeid en lui passant un objet en paramètre. typeid renvoit un objet de type typeinfo. On peut ensuite comparer plusieurs de ces objets entre eux et/ou obtenir une chaîne de caractères descriptive en invoquant la méthode name(). On peut bien sûr utiliser la RTTI sur des types de base mais là où elle est le plus utile est dans le cas du polymorphisme. Voici un exemple :

#include <typeinfo>
#include <iostream>
using namespace std;

class A {
public:
    virtual void do_it() { cout << "I'm an A" << endl; }
};
class B : public A {
public:
    void do_it() { cout << "I'm a B" << endl; }
};

class C : public A {
public:
    void do_it() { cout << "I'm a C" << endl; }
};

int main(void) {
    A a;
    B b;
    C c;
    A* obj;

    cout << typeid(obj).name() << endl;
    obj = &a;
    cout << typeid(*obj).name() << endl;
    obj = &b;
    cout << typeid(*obj).name() << endl;
    if (typeid(*obj) == typeid(b))
        cout << "on pointe un B" << endl;
    obj = &c;
    cout << typeid(*obj).name() << endl;

    return 0;
}

Une compilation/exécution donne :

$ g++ -o rtti rtti.c++
$ ./rtti
P1A
1A
1B
on pointe un B
1C

Cet exemple est simple mais pourtant très intéressant. On y voit la déclaration d'un pointeur sur A, obj puis l'appel pour ce pointeur de typeid. Le type A* est correctement identifié (P1A).

Pour connaître le type effectif de l'objet pointé, on doit appeler typeid en lui passant *obj en paramètre, et non le pointeur lui même. En faisant cela, on remarque que l'on a bien le type de l'objet pointé (1A, 1B et 1C).

On trouve également dans l'exemple l'utilisation de la comparaison de deux objets typeinfo, en guise d'illustration.

Il est important de noter que cet exemple fonctionne car la méthode do_it de la classe A est virtuelle, ce qui fait que la type d'objet pointé est déterminé lors de l'exécution et non lors de la compilation. Si la classe A ne comportait aucune méthode virtuelle, typeid appelée pour *obj renverrait toujours le type correspondant à la classe A.

const

Ce mot clé simple a plusieurs utilisations. La première, évidente, est de définir des variables "constantes". C'est à dire des variables qui ne peuvent pas être modifiées par le programmeur.

1: class B : public A {
2: 	const int mavar;
3: public:
4: 	B() : mavar(5) {};
5: 	void do_it();
6: };

L'initialisation des membres constants doit être faite dans la liste d'initialiseurs du constructeur, comme dans l'exemple ci-dessus. Remplacer la ligne 4 par la syntaxe suivante provoque une erreur lors de la compilation.

B() {mavar=5};
$ g++ -o const const.c++
const.c++: In method `B::B()':
const.c++:12: assignment of read-only member `B::mavar'
const.c++:12: parse error before `}'

On utilise également le mot clé const pour préciser qu'un paramètre de fonction ne doit pas être modifié, c'est le cas dans la déclaration suivante :

void do_it (const int& param);

Une tentative de modification d'un tel paramètre dans la définition de la fonction se traduirait par une erreur à la compilation semblable à :

const2.c++: In method `void B::do_it(const int &)':
const2.c++:13: assignment of read-only reference `param'

Une autre utilisation courante de const sert à préciser qu'une méthode ne modifie pas l'objet dont elle est membre lors de son exécution.

class B {
    int mavar;
public:
    int get_mavar() const;
};

Cette déclaration précise que l'appel de get_mavar ne modifie pas l'instance de B qui l'appelle.

Enfin, on peut utiliser const pour la valeur de retour d'une fonction.

#include <iostream>
using namespace std;

class D {
    char it;
public:
    D() : it('c') {};
    const char& get_it() { return it; }
    void use_it (char& it) { cout << it << endl; }
};

int main(void) {
    D d;
    d.use_it(d.get_it());
    return 0;
}

Ce code ne compile pas car use_it prend une référence sur caractère en paramètre, alors que get_it renvoie une référence sur caractère constante. On peut dans ce cas, soit copier le retour de get_it dans une variable et la passer à use_it, soit modifier la déclaration de use_it par :
void use_it (const char& it) puisque cette dernière ne modifie pas le paramètre. Si on ajoute tous les const adaptés dans l'exemple ci dessus, on obtient :

#include <iostream>
using namespace std;

class D {
    const char it;
public:
    D() : it('c') {};
    const char& get_it() const { return it; }
    void use_it (const char& it) const { cout << it << endl; }
};

int main(void) {
    D d;
    d.use_it(d.get_it());
    return 0;
}

Vous l'aurez compris, l'usage de ce mot clé permet de limiter grandement les bogues dans les programmes conséquents, en permettant de fixer l'accès "en lecture seule" à certaines variables.

mutable

C'est mon mot clé préféré en C++. La première rencontre de ce mot clé fait souvent se poser des questions au "c++newbie". L'explication a également de quoi dérouter. Ce mot clé sert à rendre variables certains membres, d'un objet constant. On peut modifier des membres mutables d'un objet constant par le biais de méthodes const.

Un exemple sera plus parlant :

#include <iostream>
using namespace std;

class Player {
public:
    typedef enum {RedColor, BlueColor, GreenColor} ColorItem;

private:
    ColorItem color;
    mutable int position[3];

public:
    Player(ColorItem c) : color(c) { setPosition(0,0,0); };
    void setPosition(int x, int y, int z) const;
};

void Player::setPosition(int x, int y, int z) const {
    position[0] = x;
    position[1] = y;
    position[2] = z;
}

int main(void) {
    const Player p1(Player::RedColor);

    p1.setPosition(5, 6, 0);

    return 0;
}

On déclare et définit, dans l'exemple ci-dessus, une classe Player. Un joueur est constant tout au long d'une partie. Un joueur est identifié entre autres paramètres, par sa couleur. En outre, un joueur possède une position qui, elle, change (sinon, le jeu est monotone). On déclare donc la position mutable, puisqu'on veut pouvoir la modifier et que l'objet à qui elle appartient est, lui, globalement constant. Sans le mot clé mutable, le code précédent ne peut être compilé puisque p1.setPosition(5, 6, 0); modifie l'objet p1, constant.

inline

Le mot clé inline est utilisé pour optimiser certaines parties de code. Si une fonction est déclarée inline alors chaque appel de cette fonction sera remplacée par son contenu. Cela évite un appel de fonction avec les opérations nécessaires sur la pile et optimise donc le code. Notons qu'une méthode définie dans la déclaration de la classe est implicitement inline.

Peut être reconnaissez vous là un mécanisme proche des #define en C (utilisables aussi en C++, soit dit en passant). L'apport des fonctions inline est qu'au sein de ses dernières, on peut avoir accès aux membres privés, alors que ce serait impossible en utilisant les macros classiques. Par ailleurs, les macros classiques sont traitées par le préprocesseur et les fonctions inline par le compilateur.

virtual

Ce mot clé est placé avant une méthode pour indiquer que les classes dérivées sont susceptibles d'en fournir une implémentation différente et assurer ainsi que la bonne méthode sera appelée. On parle parfois de liaison dynamique...

Outre l'exemple vu dans le paragraphe Polymorphisme, je vous propose ici un autre exemple d'utilité du mot clé virtual :

#include <iostream>
using namespace std;

class A {
public:
    void do_it2() { cout << "I'm an A" << endl; }
    void do_it() { do_it2(); }
};

class B : public A {
public:
    void do_it2() { cout << "I'm a B" << endl; }
};

int main(void) {
    B b;

    b.do_it();

    return 0;
}

On définit une classe A contenant deux méthodes do_it et do_it2. La classe B hérite de A et redéfinit la méthode do_it2 pour la faire correspondre à ses besoins.

Dans le main, on crée une instance b de B et on appelle la méthode do_it, héritée de A. On obtient l'affichage suivant :

$ g++ -o virtual virtual.c++
$ ./virtual
I'm an A

Vous comprenez que ce n'est pas du tout ce qu'on veut ... En fait la méthode do_it de la classe A appelle la méthode do_it2 "la plus proche".

Pour que la méthode do_it2 appelée soit celle de B, on doit déclarer do_it2 comme virtual dans A (et également dans B si on compte en dériver d'autres classes).

Ce phénomène est connu sous le nom de liaison dynamique.

Une fois cette modification faite (void do_it2() {...), on obtient la sortie atendue :

$ g++ -o virtual virtual.c++
$ ./virtual
I'm a B

On peut également déclarer des classes virtuelles. Je vous laisse vous reporter à une autre source de documentation sur ce sujet car je le trouve plus propice à erreur qu'autre chose et n'ai donc jamais vraiment creusé le sujet.

static

Le mot clé static a, comme beaucoup d'autres, plusieurs utilités.

La première est la même qu'en C. Il ne modifie pas la portée des variables mais assure que leur valeur n'est pas perdue pendant toute la vie du programme. Ainsi si une variable est déclarée static dans une méthode, sa valeur se conserve d'un appel à l'autre de cette dernière :

#include <iostream>
using namespace std;

void compte() {
    static int nb_fois = 0;

    cout << "Appel n°" << ++nb_fois << endl;
}

int main(void) {
	
    for (int i=1; i!=5; ++i) compte();

    return 0;
}

Compilation et exécution :

$ g++ -o static static.c++
$ ./static
Appel n°1
Appel n°2
Appel n°3
Appel n°4

L'exemple parle de lui même, on se rend compte que la valeur de nb_fois est conservée d'un appel à l'autre de la fonction compte.

Vous pouvez également remarquez un point TRES important : Une variable statique n'est initialisée que lors de la première occurence (sinon, elle reviendrait à 0 à chaque appel de compte.

Une propriété de classe déclarée static a une valeur commune pour toutes les instances de classe, elle est partagée.

#include <iostream>
using namespace std;

class A {
    static int nb_obj;
public:
    A() { cout << "création de l'objet n°" << ++nb_obj << endl; }
    ~A() { --nb_obj; }
};

int A::nb_obj = 0;


int main(void) {
    A* p[5];

    for (int i=1; i!=5; ++i) p[i] = new A;
    for (int i=1; i!=5; ++i) delete p[i];

    return 0;
}

Remarquez la définition de la propriété statique nb_obj faite une fois pour toutes les instances de la classe A. La compilation/exécution confirme le caractère partagé de la propriété nb_obj.

$ g++ -o static_obj static_obj.c++
$ ./static_obj
création de l'objet n°1
création de l'objet n°2
création de l'objet n°3
création de l'objet n°4

Une autre utilisation de static est de limiter la portée de variables globales au fichier dans lequel elles sont définies. Consultez à ce propos l'exemple associé au mot clé extern, ci-dessous.

extern

Dans le cas de programmes écrits sur de multiples fichiers, le mot clé extern permet de rappeler dans un fichier qui les utilise des variables déclarées ailleurs.

extern1.c++

static int static_extern = 3;
int just_extern = 4;

extern2.c++

#include <iostream>
using namespace std;

extern int just_extern;

int main(void) {
    for (int i=1; i!=just_extern; ++i)
        cout << "Hi !" << endl;
    return 0;
}

Notez que, comme dit plus haut, les variables statiques ne peuvent pas être référencées en dehors du fichier dans lequel elles sont déclarées. Si vous remplacez les occurences de just_extern par static_extern dans le fichier extern2.c++, vous obtiendrez l'erreur suivante lors de la liaison :

$ g++ -o extern extern1.c++ extern2.c++
undefined reference to `static_extern'

namespace

Les namespaces ou espaces de noms permettent d'ajouter un cloisonnement logique entre divers modules ou parties d'un programme développé par un grand nombre de personnes. La STL, par exemple, définit toutes ses classes, objets et fonctions dans l'espace de noms std, c'est pour celà que pour accéder à ses objets et classes, on doit les préfixer par std:: (std::cout, std::endl, std::ifstream, ...). Afin de se passer d'écrire ce préfixe, on utilise la directive using namespace suivie du nom de l'espace de noms que l'on souhaite utiliser. Ainsi, lorsque vous tapez cout, le compilateur le recherche automatiquement dans l'espace de noms std. Toutefois, écrire à chaque fois std:: permet d'éviter les conflits, si vous accédez à un objet existant dans plusieurs espaces de noms.

Vous pouvez vous aussi définir vos espaces de noms. Voyons un exemple. Imaginez Xavier Garreau et Yves Mettier bossant ensemble sur un projet (c'est une vue de l'esprit, ça n'arrive pas en vrai). Chacun crée son propre espace de noms, afin que le programme résultant ne contienne pas de conflits dans le nommage des divers éléments développés. Ils doivent chacun déclarer une variable, une classe et une fonction. Par un hasard incroyable, ils leurs donnent des noms similaires. Sans les espaces de nommage, celà crée des conflits, avec non.

#include <iostream>
using namespace std;

namespace xg {
    int ma_var = 5;
    void uneFunc() { cout << "xg " << ma_var << endl; }
};

namespace ym {
    int ma_var = 7;
    class uneClasse {};
    void uneFunc() { cout << "ym " << ma_var << endl; }
};

namespace xg {
    class uneClasse {};
};

int main(void) {
    xg::uneFunc();
    ym::uneFunc();

    using namespace xg;
    uneFunc();
//    using namespace ym;
//    uneFunc();			

    return 0;
}

Xavier Garreau et Yves Mettier se définissent chacun les namespaces "xg" et "ym" et en fournissent le contenu. Remarquez qu'on peut rouvrir un namespace plusieurs fois pour le compléter, comme c'est le cas ci-dessus pour ajouter la classe uneClasse dans le namespace xg.

On voit dans le main l'utilisation des éléments des deux namespace. Pour s'affranchir du préfixe xg::, on utilise une directive using namespace, à partir de ce point, on cherche automatiquement les noms dans l'espace xg et on peut donc appeler directement uneFunc.

Décommenter les deux lignes suivantes provoque en revanche une erreur à la compilation puisqu'il y a un conflit entre xg::uneFunc et ym::uneFunc.

Conclusion

Voilà, nous sommes arrivés au terme de cet article. Je vous laisse fouiller dans les références et sur google pour compléter les notions que je vous ai exposées et vous donne rendez vous bientôt pour d'autres aspects du C++ (sockets, multithreading, interfaces graphiques, ...)

@+

Xavier Garreau - <xavier@xgarreau.org>
http://www.xgarreau.org/

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.fr/

Membre fondateur du ROCHELUG
http://www.rochelug.org/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 10.09.2004