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...

10 réponses

Avatar
Jean-Marc Bourguet
Olivier Azeau writes:

Tout à fait, même si j'essaie de ne pas en abuser de ce
pattern un peu décrié ("pourquoi rajouter une méthode
'accept' qui permet de rajouter dynamiquement des méthodes
sur la hiérarchie plutôt que de rajouter directement ces
méthodes dans les classes de la hiérarchie ?")
Encore une attitude que je n'ai pas rencontrée. Je vais

finir par penser que mon environnement est meilleur que je
ne le pensais :-)


Quelle attitude ?


"pourquoi rajouter une méthode..."

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org



Avatar
Loïc Joly
wrote:

Peut-on comparer "la plupart des programmes font bien peu de
choses" à "le coût du tout-objet tout-dynamique est (souvent)
redhibitoire" ?



Peut-être. (Mais disons qu'il faudrait commencer par définir
« bien peu de choses ». Le programme dans ton portable fait tout
ce qu'il faut, et probablement pas mal en plus, sans pour autant
qu'il surcharge le CPU ni qu'il ait besoin des mésures
d'optimisation particulière.)


Pour info, je suis mécontent de mon portable actuel. Il met jusqu'à 8s
pour naviguer d'un menu à l'autre. Le prochain protable que j'achète, je
fais le test en magasin et j'en fait un critère majeur.


[...]

Le traitement d'image c'est un domaine que j'inclurai sous la
vocable « numérique » ; c'est en tout cas un domaine où les
cycles CPU jouent un rôle. En revanche, en robotique... Ce
n'était pas le cas dans les applications que je connais dans le
domaine (robots de construction automobile).


La robotique est AMA un domaine en train de demander de plus en plus de
puissance de calcul. L'idée à long terme est bien qu'on dise à un robot
: Voici la CAO de la pièce, débrouille toi.

--
Loïc


Avatar
Jean-Marc Bourguet
Loïc Joly writes:

Gabriel Dos Reis wrote:

Ce que je sais de source sûre, c'est qu'il avait décidé de ne pas
tomber dans la trappe, comme Eiffel, avec la contravariance.


Pourrais tu préciser ce qu'est la contravariance, s'il te plait ?


struct C {
};

struct D: C {
};

struct C2 {
virtual void f(D);
};

struct D2: C2 {
virtual void f(C); // supplante C2::f
};

L'idée est que dans une classe dérivée on peut relacher les
préconditions tout comme on peut augmenter les contraintes
sur le résultat (ce qui donne la co-variance).

A+

--
Jean-Marc
FAQ de fclc++: http://www.cmla.ens-cachan.fr/~dosreis/C++/FAQ
C++ FAQ Lite en VF: http://www.ifrance.com/jlecomte/c++/c++-faq-lite/index.html
Site de usenet-fr: http://www.usenet-fr.news.eu.org


Avatar
Loïc Joly
wrote:

Ivan Vecerina wrote:

| wrote in message
|news:
|Ivan Vecerina wrote:
|> "James Kanze" wrote in message
|> news:42028b18$0$24299$



|> Mais qu'il s'agisse de traitement d'image, de programmation
|> graphique, de jeux, de systèmes temps-réel ou de
|> programmation multi-process, je trouve réguilérement une
|> place à la paramétrisation d'algorithmes ou de diverses
|> classes.



|Quel rapport ? Quand les templates conviennent, on utilise
|les templates. Quand l'héritage convient, on utilise
|l'héritage. Les deux sont là pour une raison, et c'est même
|plutôt rare que leurs champs d'applications se récouvrent.



On ne perd certainement pas en puissance et en flexibilité en
faisant du tout-object et tout-dynamique.



Bien sûr que si. Et comment. Il y a des choses qui ne se laisse
pas exprimer, ou au moins dont l'expression est beaucoup plus
complexe, avec l'héritage. Et vice-versa.


Cependant, même dans des applications banales, ceci a un coût
souvent rédhibitoire.



Utiliser un outil mal-approprié est toujours très cher.


C'est pour pouvoir manipuler des données (relativement) brutes
et pas tout-à-fait encapsulée derrière des objets et des
messages que l'on doit se résoudre à avoir recours à d'autres
formes de dispatching et de réutilisation de code.



D'après mes expériences : une très bonne encapsulation est
essentielle si la performance risque d'être un problème. Parce
que sans l'encapsulation, on est bloqué ; on ne peut plus faire
des modifications nécessaires une fois que le profiler a montré
où se trouve les problèmes.


Le résultat, en C++, est effectivement que l'on a des champs
d'application relativement distincts pour les templates et les
fonctions virtuelles/héritages.



Ce n'est pas un « résultat » de je ne sais pas quel problème de
performance. C'est qu'il s'agit de deux outils différents,
conçus pour résoudre des problèmes différents. Si tu as besoin
de l'héritage et des fonctions virtuelles, il y a peu de chances
que les templates font l'affaire, et vice versa. Et dans les
rares cas où l'un ou l'autre peut servir... La seule fois que
j'ai fait une mésure, les templates étaient plus rapide avec
g++, et les fonctions virtuelles avec Sun CC (qui était aussi
plus rapid globalement). Mais ça fait un moment, les versions
(g++ 2.95.2 et Sun CC 4.2) que j'ai comparé ne sont plus du tout
actuelle. C'est juste pour dire que la solution n'est pas si
évidente qu'elle puisse semblait.


|> Pour ce qui est des accès disque (lecture ou écriture), je
|> procède en général par memory-mapping (sous NT ou Unix).
|> Ceci simplifie le code et permet au systèmes d'optimiser et
|> de paralléliser les accès mémoire.



|Mais ça ne garantit rien. L'écriture n'est pas forcement
|faite. C'est bon pour des choses « jettables », mais je n'ai
|pas le droit d'informer le client que son ordre a été pris en
|compte tant que je ne peux pas garantir pouvoir le récupérer,
|même en cas de crash système.



Qu'il s'agisse de memory-mapping ou d'écriture d'une séquence
d'octets, là n'est pas la question. Sans flush il n'y a pas de
garantie, ni dans un cas ni dans l'autre.



(Attention. Je crois que ton propos peut préter à la confusion.
À ce niveau-ci, on parle plus volentiers de sync que de flush.)

Tout à fait. Sauf qu'il y a une option d'open ou de fcntl qui
fait que toute écriture est synchronisée. Tandis que s'assurer
la synchronisation avec mmap est plus complex. (Je parle de Unix
ici, parce que je n'ai pas la moindre idée comment ça se passe
sous Windows. Je suppose qu'une persistance robuste est possible
sous Windows, mais je n'ai pas la moindre idée comment y
arriver.)

Et c'est la synchronisation, évidemment, qui coûte les dix
millisecondes.


|> > Dans les applications que j'ai vu, le goulot a prèsque
|> > toujours été l'écriture disque.



Sur quoi j'ai mentionné un autre genre d'applications... la
réponse pourrait presque être amusante:



|Si tu régardes autour de toi, tu verras que la plupart des
|programmes font bien peu de choses, en somme.



Qui ne voit pas assez loin ?



Regarde autour de toi. Pas seulement au boulot, où tu travailles
peut-être dans un domaine où la performance CPU est importante,
mais plus généralement. Les ordinateurs dans ta voiture, par
exemple.


Ce n'est par l'impression que m'ont donné les fournisseurs automobiles
que j'ai pu cotoyer. Surtout que l'optique ici est que si l'on arrive à
faire tourner un calculateur ESP sur un système coûtant 1 centime moins
cher, le bénéfice se chiffre en millions d'euros.


Les serveurs de page web (ou il y a certainement
quelques un où la performance est importante, mais combien par
rapport à ceux qui ne font que de resservir des pages tout
faites ou gérer un chariot d'achat).


C'est là un domaine que je ne connais pas.

Les ordinateurs de l'état
(je suppose que tu paies des impots, ou que tu as un permit
de conduire),


Pareil, même si on se prend à rêver qu'avant même de fonctionner vite,
ils soient capables de fonctionner sans se planter...

les ordinateurs du boulanger (qui s'appelle pas
toujours par le nom d'ordinateur), ou du grande surface.


Un ami travaillant dans le domaine m'a rapporté des problèmes de
performances qu'ils avaient, sur le temps de traitement admis d'un code
barre, entre la recherche d'informations de prix centralisées, la
possibilité d'offre promotionelles tordues...

[...]


Mais ce n'était pas mon propos. Tout ce que j'ai dit, c'est que
la plupart des programmeurs n'en sont pas directement concernés
par la différence en vitesse entre un appel virtuel et un appel
non-virtuel.


Probabement.

--
Loïc


Avatar
Olivier Azeau
Jean-Marc Bourguet wrote:
Olivier Azeau writes:


Tout à fait, même si j'essaie de ne pas en abuser de ce
pattern un peu décrié ("pourquoi rajouter une méthode
'accept' qui permet de rajouter dynamiquement des méthodes
sur la hiérarchie plutôt que de rajouter directement ces
méthodes dans les classes de la hiérarchie ?")


Encore une attitude que je n'ai pas rencontrée. Je vais
finir par penser que mon environnement est meilleur que je
ne le pensais :-)


Quelle attitude ?



"pourquoi rajouter une méthode..."


Donc se poser la question de rajouter des méthodes à une hiérarchie de
classes par rapport à accepter des visiteurs sans se demander si on
n'alourdit pas l'ensemble de la conception ne te semble pas une pratique
intéressante ?




Avatar
Loïc Joly
Loïc Joly wrote comme un sagouin...

Désolé pour avoir oublié de nettoyer le message auquel je répondais...

--
Loïc
Avatar
Ivan Vecerina
wrote in message
news:
:Mais ce n'était pas mon propos. Tout ce que j'ai dit, c'est que
:la plupart des programmeurs n'en sont pas directement concernés
:par la différence en vitesse entre un appel virtuel et un appel
:non-virtuel.

Vraiment? Je trouve que c'est là une interprétation bien
restrictive et partiale de ce que tu as écrit:

wrote in message
news:
:Si tu régardes autour de toi, tu verras que la plupart des
:programmes font bien peu de choses, en somme. En revanche, ils
:communiquent ce qu'ils ont fait (accès réseau) et ils le sauvent
:(écriture disque). Et que dans la pratique, ce sont les deux
:choses qui rallentire le plus.

A mon humble avis, il y a un monde entre "n'être affecté que par
les entrées-sorties" et "ne pas être directement concerné par la
différence entre un appel virtuel et un appel non-virtuel".

Je ne pense pas que ce soit par malhonnêteté. Mais je suis
d'avis que tu devrais faire preuve de plus de cohérence,
de nuance, et sans doute de modération dans tes propos.

--
http://ivan.vecerina.com/contact/?subject=NG_POST <- email contact form
Avatar
Olivier Azeau
Loïc Joly wrote:
wrote:
Regarde autour de toi. Pas seulement au boulot, où tu travailles
peut-être dans un domaine où la performance CPU est importante,
mais plus généralement. Les ordinateurs dans ta voiture, par
exemple.


Ce n'est par l'impression que m'ont donné les fournisseurs automobiles
que j'ai pu cotoyer. Surtout que l'optique ici est que si l'on arrive à
faire tourner un calculateur ESP sur un système coûtant 1 centime moins
cher, le bénéfice se chiffre en millions d'euros.


Je ne connais pas du tout ce domaine mais rien que l'idée de savoir que
quelqu'un pourrait envisager de me vendre une voiture quelques euros de
moins en prenant le risque de rajouter un bug suite à une optimisation
de code me fait un peu froid dans le dos...

Les serveurs de page web (ou il y a certainement
quelques un où la performance est importante, mais combien par
rapport à ceux qui ne font que de resservir des pages tout
faites ou gérer un chariot d'achat).


C'est là un domaine que je ne connais pas.


En fait la perf est parfois importante quand il s'agit de traiter des
requêtes en nombre (sinon à quoi serviraient les load balancers ?) mais
visiblement le problème n'atteint pas les couches applicatives
(d'ailleurs du code applicatif en C++ sur du serveur web ça ne court pas
les rues...)

Les ordinateurs de l'état
(je suppose que tu paies des impots, ou que tu as un permit
de conduire),


Pareil, même si on se prend à rêver qu'avant même de fonctionner vite,
ils soient capables de fonctionner sans se planter...


Je crois que tu as mis le doigt sur le point important : avant de se
demander si le boulot est fait assez vite, on se demande toujours si le
boulot est fait correctement, et une optimisation est un risque de bug
supplémentaire.

les ordinateurs du boulanger (qui s'appelle pas
toujours par le nom d'ordinateur), ou du grande surface.


Un ami travaillant dans le domaine m'a rapporté des problèmes de
performances qu'ils avaient, sur le temps de traitement admis d'un code
barre, entre la recherche d'informations de prix centralisées, la
possibilité d'offre promotionelles tordues...


Le problème était-il vraiment un problème de perf CPU sur l'ordinateur
du boulanger ?
Je pense que personne n'osera incriminer des appels virtuels C++ (du
moins pas avant d'avoir envisagé 50 autres pistes) sur un système qui a
probablement plus de couches qu'un mille feuille et des I/O dans tous
les sens.

Mais ce n'était pas mon propos. Tout ce que j'ai dit, c'est que
la plupart des programmeurs n'en sont pas directement concernés
par la différence en vitesse entre un appel virtuel et un appel
non-virtuel.


Probabement.


Pas mieux.


Avatar
Gabriel Dos Reis
"Ivan Vecerina" writes:

[...]

| Je ne pense pas que ce soit par malhonnêteté. Mais je suis

il est probable qu'il te dira qu'il fait de l'hyperbole...

-- Gaby
Avatar
Loïc Joly
Olivier Azeau wrote:

J'ai réordonné mes réponses dans un sens plus logique :


Je crois que tu as mis le doigt sur le point important : avant de se
demander si le boulot est fait assez vite, on se demande toujours si le
boulot est fait correctement, et une optimisation est un risque de bug
supplémentaire.


Je dirais plutôt qu'une optimisation est un risque de coût de
développement supplémentaire, et que ce développement sera fait à
vitesse moindre, pour éviter les bugs dans une zone un peu plus dangereuse.

Je ne connais pas du tout ce domaine mais rien que l'idée de savoir que
quelqu'un pourrait envisager de me vendre une voiture quelques euros de
moins en prenant le risque de rajouter un bug suite à une optimisation
de code me fait un peu froid dans le dos...


Dans un logiciel vendu en grand nombre, le coût de développement
supplémentaire (unique) est probablement amorti par rapport au coût de
hard (par système). Par contre, je ne pense pas que la fiabilité s'en
ressente.


les ordinateurs du boulanger (qui s'appelle pas
toujours par le nom d'ordinateur), ou du grande surface.



Un ami travaillant dans le domaine m'a rapporté des problèmes de
performances qu'ils avaient, sur le temps de traitement admis d'un
code barre, entre la recherche d'informations de prix centralisées, la
possibilité d'offre promotionelles tordues...



Le problème était-il vraiment un problème de perf CPU sur l'ordinateur
du boulanger ?
Je pense que personne n'osera incriminer des appels virtuels C++ (du
moins pas avant d'avoir envisagé 50 autres pistes) sur un système qui a
probablement plus de couches qu'un mille feuille et des I/O dans tous
les sens.


Je ne sais pas les détails, mais même si je pense qu'un appel virtuel
C++ n'est probablement pas en cause (mon pote bosse en VB de mémoire),
je ne répondais pas à "Le coût d'une fonction virtuelle est en général
acceptable" mais à "Sauf dans des domaines particuliers, les
performances ne comptent pas de nos jours".

--
Loïc