OVH Cloud OVH Cloud

La fin de l'héritage ?

103 réponses
Avatar
Olivier Azeau
J'ai lu avec grand intérêt l'article "Concepts for C++0x" mentionné dans
un thread récent.
Habituellement, je ne m'intéresse pas aux évolutions du langage car
elles ne me concernent qu'à un relativement long terme, mais là, j'ai
l'impression qu'un mouvement de fond relatif aux paradygmes du C++ prend
une certaine ampleur.
Et ce mouvement m'amène à quelques interrogations.

La plupart des développeurs que je cotoie programment en C++ dans un
style orienté objet très proche de ce que l'on trouve en Java.

Je prends un exemple classique de Bridge+Factory pour illustrer mon propos.
On a typiquement une hiérarchie d'implémentations :
| class DocumentDisplay {
| virtual ~DocumentDisplay();
| virtual void drawText( int x, int y, std::string const &text ) = 0;
| virtual void drawLine( int x1, int y1, int x2, int y2 ) = 0;
| };
|
| class GraphicDocumentDisplay : public DocumentDisplay {
| virtual void drawText( int x, int y, std::string const &text );
| virtual void drawLine( int x1, int y1, int x2, int y2 );
| };
|
| class TextDocumentDisplay : public DocumentDisplay {
| ...

une hiérarchie d'abstractions qui utilisent les implémentations :
|
| class Document {
| public:
| Document( DocumentDisplay *display ) : display_(display) {}
| virtual ~Document();
|
| void drawFrame( int x, int y, int w, int h, std::string const &title ) {
| display_->drawLine( x, y, x+w, y );
| display_->drawLine( x, y+h, x+w, y+h );
| display_->drawText( x, y, title );
| }
| private:
| DocumentDisplay *display_;
| };
|
| class Memo : public Document {
| public:
| Memo( DocumentDisplay *display ) : Document(display) {}
| void drawSummary() { drawFrame( 0, 0, 150, 100, "Summary" ); }
| };
|
| class Invoice : public Document {
| ...

une hiérarchie de fabriques pour instancier tout ça :
| class DocumentFactory {
| public:
| virtual ~DocumentFactory() {}
| virtual Memo *createMemo() = 0;
| virtual Invoice *createInvoice() = 0;
| };
|
| class GraphicDocumentFactory : public DocumentFactory {
| public:
| virtual Memo *createMemo() { return new Memo( new
GraphicDocumentDisplay ); }
| virtual Invoice *createInvoice() { return new Invoice( new
GraphicDocumentDisplay ); }
| };

et je rajoute un programme principal pour utiliser le tout :
| class Application {
| public:
| Application( DocumentFactory *factory ) : factory_(factory) {}
|
| void run() {
| Memo *memo = factory_->createMemo();
| memo->drawSummary();
| }
| private:
| DocumentFactory *factory_;
| };
|
| int main() {
| Application app( new GraphicDocumentFactory );
| app.run();
| }

En pratique, ça a une tête un peu différente (avec le RAII par exemple),
mais l'idée des hiérarchie de classes est là.

Depuis quelques temps se répand la programmation générique qui permet de
faire la même chose avec (en général) moins de lignes de code.

Les implémentations deviennent des "policy" et n'ont plus besoin de
classe de base
| class GraphicDocumentDisplay {
| virtual void drawText( int x, int y, std::string const &text );
| virtual void drawLine( int x1, int y1, int x2, int y2 );
| };

Les abstractions sont paramétrées par la policy.
On garde un héritage pour partager l'implémentation :
| template <class DOCDISP>
| class Document {
| public:
| virtual ~Document() {}
|
| void drawFrame( int x, int y, int w, int h, std::string const &title ) {
| display_.drawLine( x, y, x+w, y );
| display_.drawLine( x, y+h, x+w, y+h );
| display_.drawText( x, y, title );
| }
| private:
| DOCDISP display_;
| };
|
| template <class DOCDISP>
| class Memo : public Document<DOCDISP> {
| public:
| void drawSummary() { drawFrame( 0, 0, 150, 100, "Summary" ); }
| };

Dans un cas aussi simple, on oublie la factory et on paramètre
directement l'application :
| template <class DOCDISP>
| class Application {
| public:
| void run() {
| Memo<DOCDISP> memo;
| memo.drawSummary();
| }
| };
|
| int main() {
| Application<GraphicDocumentDisplay> app;
| app.run();
| }

Une telle approche est actuellement plutôt prisée par des personnes qui
connaissent bien le langage et est donc globalement plutôt minoritaire.
Parmi les raisons de cette situation je vois :
- la forte présence de langages comme Java ou C# qui ne proposent pas
cette approche (les versions "Generic" restent anecdotiques) et donc le
grand nombre de personnes qui font du C++ comme ils font du Java
- le faible support des templates par certains compilateurs jusqu'à une
période récente (cf les interrogations récurrentes relatives au support
des templates dans VC6)
- un support méthodologique peu adapté (UML se prête beaucoup plus à
décrire des héritages que de paramétrages)
- une complexité de mise au point d'une approche à base de templates
(typage structurel, définitions statiques, ...)

Sur ce, je découvre les "concepts" (dont je ne pense pas avoir saisi le
dixième des utilisations et implications) qui me laissent supposer que,
dans un avenir plus ou moins proche, on pourrait écrire les choses de
manière plus explicites.

Les policy pourraient être nommées et contrôlées avant usage. Si j'ai
bien compris les notions et syntaxes proposées dans l'article, cela
donnerait quelque chose comme :
| template <DOCDISP>
| concept DisplaysDocuments {
| void DOCDISP::drawText( int x, int y, std::string const &text );
| void DOCDISP::drawLine( int x1, int y1, int x2, int y2 );
| };
|
| class GraphicDocumentDisplay {
| public:
| void drawText( int x, int y, std::string const &text );
| void drawLine( int x1, int y1, int x2, int y2 );
| };
|
| model DisplaysDocuments<GraphicDocumentDisplay>;

| template <class DOCDISP>
| class Application where { DisplaysDocuments<DOCDISP> } {
| public:
| void run() {
| Memo<DOCDISP> memo;
| memo.drawSummary();
| }
| };

Et j'ai même l'impression que des implémentations par défaut définies au
niveau des concepts pourraient rendre obsolète une grand pan de
l'utilisation de l'héritage.

En écrivant par exemple ce qui suit, j'ai l'impression d'avoir
entièrement réécrit l'exemple vu précédemment sans utiliser aucun
héritage C++ mais avec l'impression d'avoir quand même fait de l'"objet"
(s'il est encore possible de définir ce terme...) mais "autrement".
| template <DOC>
| concept IsDocument {
| typename doc_display;
| require DisplaysDocuments<doc_display>;
|
| doc_display &DOC::display();
|
| void DOC::drawFrame( int x, int y, int w, int h, std::string const
&title ) {
| display().drawLine( x, y, x+w, y );
| display().drawLine( x, y+h, x+w, y+h );
| display().drawText( x, y, title );
| }
| };
|
| template <class DOCDISP>
| class Memo {
| public:
| typedef DOCDISP doc_display;
| doc_display &display() { return display_; }
|
| void drawSummary() { drawFrame( 0, 0, 150, 100, "Summary" ); }
| private:
| DOCDISP display_;
| };
|
| template <class DOCDISP>
| model IsDocument< Memo<DOCDISP> >;

Pour ceux qui auront eu le courage de lire jusqu'ici, j'aimerais savoir
s'ils pensent :
- que je n'ai rien compris aux "concepts" ?
- que ces notions vont avoir un impact technique majeur sur l'écriture
de code en C++ ?
- que ces notions vont avoir un impact sociologique majeur sur le
développement en C++ ?

Je suis plus particulièrement intéressé par l'aspect sociologique des
choses.
J'ai l'impression d'être face à une évolution du même ordre de grandeur
que le passage du C au C++.
Pour tout dire, j'ai même l'impression que le terme C++ n'est conservé
que pour des raisons marketing (la "marque" est déja connue, appréciée,
possède une base de consommateurs, ...)

Il est toujours possible d'écrire du C avec un compilateur C++ mais, sur
une période d'environ 10 ans (en gros les années 90) on est passé d'une
approche majoritaire en termes de structures/procédures a une approche
majoritaire classes/héritages/associations.
Cela a impliqué un changement de principes de modélisation, un
changement de techniques d'écriture de code mais surtout un changement
de mentalité.
Et quand je regarde les efforts qu'il a fallu déployer pour que
l'ensemble des intervenants en arrive à penser les développements plus
ou moins de la même manière, j'ai un peu l'impression, en voyant ces
nouvelles notions qui se profilent à l'horizon, que nous ne sommes pas
au bout de nos peines...

3 réponses

7 8 9 10 11
Avatar
Olivier Azeau
del yahoo wrote:
Sans prétendre que l'un est meilleur que l'autre (même si j'ai mon
avis sur la question), le fait est que le programmeur C++ "de base"
(je mets entre guillemets ) est habitué au sous-typage déclaratif,
puisque c'est celui qui est utilisé dans l'héritage public pour
le sous-typage dynamique. Que le sous-typage statique (template)
offre cette voie là me semble un bon point.


C'est vrai (je n'avais pas envisagé ce point là)

Néanmoins, un reproche au sous-typage déclaratif est son aspect
verbeux. Pour les débutants, la majorité des classes seront
DefautConstructible et Assignable (1). Je pense aux gens qui
désireraient se mettre aux concepts. S'il faut rajouter "models
Assignable, DefautConstructible" sur (presque) toutes les classes
d'un projet, pour pouvoir utiliser vector, les gens hésiteront
surement, non ?


Mais, pour rester cohérent, ça ne devrait pas être implicite (tout comme
les actuels contructeurs par défaut, de copie et operateur d'affectation) ?

Mais c'est le seul bémol que je vois à la proposition. Elle me semble
aller dans le bon sens sinon. J'ai noté que le papier proposait
de définir une interface et pas simplement des cas d'utilisations
(constraints), ce qui me semble le bon choix.


Je les ai lus après coup et je trouve ça plutôt illisible (mais comme il
y a plein d'autres trucs que je trouve illisibles... ;-)

En fait, ça fait quelque temps que je dis, quand on me demande pourquoi
je m'intéresse tant à C++ que je dis qu'un champs d'expérimentation sur
la généricité y a été ouvert. La généricité était présente dans d'autres
languages (Eiffel, Ada), mais sous une forme plus contrainte (mais on
savait où on allait). Là, C++ semble avoir la maturité sur le sujet pour
transformer l'expérimentation en technique maitrisée.


Le C++ peut être, mais les utilisateurs du C++ ?

[...]
Non, pas que. Il y a aussi (surtout ?) la compatibilité asendante:
on pourra avoir dans le même code des bouts "anciens" (OO et templates
limités à l'utilisation de vector et map) et des bouts "nouveaux".


Oui... D'ailleurs aujourd'hui on est en général très satisfaits d'avoir
des bouts anciens en C et des bouts nouveaux en C++ dans un même appli :-/

Avatar
kanze
Olivier Azeau wrote:
del yahoo wrote:
Sans prétendre que l'un est meilleur que l'autre (même si
j'ai mon avis sur la question), le fait est que le
programmeur C++ "de base" (je mets entre guillemets ) est
habitué au sous-typage déclaratif, puisque c'est celui qui
est utilisé dans l'héritage public pour le sous-typage
dynamique. Que le sous-typage statique (template) offre
cette voie là me semble un bon point.


C'est vrai (je n'avais pas envisagé ce point là)


J'ai lu quelque part que les utilisateurs des langages à
sous-typage déclaratif sous-estîme systèmatiquement la souplesse
du sous-typage dynamique, et les utilisateurs des langages à
sous-typage dynamique sous-estîme systèmatiquement la sécurité
qu'apporte le sous-typage déclaratif.

Les deux ont leur rôle à jouer, et la distinction est
orthogonale du type de sous-typage au sens de Cardelli et
Wagner.

Néanmoins, un reproche au sous-typage déclaratif est son
aspect verbeux. Pour les débutants, la majorité des classes
seront DefautConstructible et Assignable (1). Je pense aux
gens qui désireraient se mettre aux concepts. S'il faut
rajouter "models Assignable, DefautConstructible" sur
(presque) toutes les classes d'un projet, pour pouvoir
utiliser vector, les gens hésiteront surement, non ?


Mais, pour rester cohérent, ça ne devrait pas être implicite
(tout comme les actuels contructeurs par défaut, de copie et
operateur d'affectation) ?


Comme d'habitude, il y a des raisons historique. Une classe en
C++, c'est une struct en C. Et les struct en C ont bien un
opérateur d'affectation implicite ; puisque le C ne distingue
pas entre l'affectation et l'initialisation de la même façon que
le C++, ça fait qu'on a aussi besoin d'un constructeur de
copie.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


Avatar
marc.boyer.news
Olivier Azeau wrote in message news:<zZ9Rd.25139$...
del yahoo wrote:
Sans prétendre que l'un est meilleur que l'autre (même si j'ai mon
avis sur la question), le fait est que le programmeur C++ "de base"
(je mets entre guillemets ) est habitué au sous-typage déclaratif,
puisque c'est celui qui est utilisé dans l'héritage public pour
le sous-typage dynamique. Que le sous-typage statique (template)
offre cette voie là me semble un bon point.


C'est vrai (je n'avais pas envisagé ce point là)


Et ça me semble un argument pédagogique (promotionnel ?) important.

Néanmoins, un reproche au sous-typage déclaratif est son aspect
verbeux. Pour les débutants, la majorité des classes seront
DefautConstructible et Assignable (1). Je pense aux gens qui
désireraient se mettre aux concepts. S'il faut rajouter "models
Assignable, DefautConstructible" sur (presque) toutes les classes
d'un projet, pour pouvoir utiliser vector, les gens hésiteront
surement, non ?


Mais, pour rester cohérent, ça ne devrait pas être implicite (tout comme
les actuels contructeurs par défaut, de copie et operateur d'affectation) ?


Ce serait bien, mais comment faire ?
Supposons que, par défaut, une classe implémente toujours le model
Assignable, comment écrire si elle ne l'est pas ? Eventuellement avec
les opération de copie "private" ?
model NonAssignable {
private:
NonAssignable(const NonAssignable&);
// etc...
}

Mais c'est le seul bémol que je vois à la proposition. Elle me semble
aller dans le bon sens sinon. J'ai noté que le papier proposait
de définir une interface et pas simplement des cas d'utilisations
(constraints), ce qui me semble le bon choix.


Je les ai lus après coup et je trouve ça plutôt illisible (mais comme il
y a plein d'autres trucs que je trouve illisibles... ;-)


Que troues-tu illible ? Les contraintes ? Oui, c'est clair que c'est
pas génial, mais ça marche avec C++ actuel, et en passant, ça m'a
bien aidé pour la reprise d'une code avec plein de templates
et peu de doc.

En fait, ça fait quelque temps que je dis, quand on me demande pourquoi
je m'intéresse tant à C++ que je dis qu'un champs d'expérimentation sur
la généricité y a été ouvert. La généricité était présente dans d'autres
languages (Eiffel, Ada), mais sous une forme plus contrainte (mais on
savait où on allait). Là, C++ semble avoir la maturité sur le sujet pour
transformer l'expérimentation en technique maitrisée.


Le C++ peut être, mais les utilisateurs du C++ ?


Bien sur, c'est pas encore fait.
Mais je pense que C++ avec concepts sera plus facile à apprendre
que C++ avec template actuel.
Et puis c'est le coup de l'amorcage de la pompe: sans outil, pourquoi
un programmeur professionnel étudierait-il de pret une fonctionnalité
d'un langage ? Voilà, une fois qu'on aura une proposition dans la norme,
on aura des compilateurs, et puis on pourra s'en servir.


Non, pas que. Il y a aussi (surtout ?) la compatibilité asendante:
on pourra avoir dans le même code des bouts "anciens" (OO et templates
limités à l'utilisation de vector et map) et des bouts "nouveaux".


Oui... D'ailleurs aujourd'hui on est en général très satisfaits d'avoir
des bouts anciens en C et des bouts nouveaux en C++ dans un même appli :-/


N'est-ce pas.

Marc Boyer


7 8 9 10 11