Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Question entretien C++ - doutes.

95 réponses
Avatar
David Fleury
Bonjour,

voici une des questions que je pose pour lors de mes entretiens pour des
développeurs avec expérience orienté C++

"Ecrire une fonction qui renverse une chaine (ex: ABC -> CBA, AE -> EA, ...)

Après avoir posé la question deux ou trois fois déjà, je suis surpris
des réponses (souvent fausses). Je laisse volontairement le choix au
candidat d'utiliser (char* ou std::string) et/ou une fonction modifiant
la chaine entrée ou retournant une nouvelle chaine, et l'utilisation de
la STL est autorisé.

A votre avis, est-ce que la question n'est pas trop mauvaise ?

David.

PS : Dans le même genre, il y a la fonction EstPalindrome qui semble
bloquer.

10 réponses

Avatar
James Kanze
On Oct 30, 5:31 pm, Wykaaa wrote:
Michael DOUBEZ a écrit :
>>> [...]



>>>> En particulier avec C++, la capacite a faire un bon design est
>>>> essentielle dans un "vrai" projet.
>>> Ce n'est pas particulier avec C++ ; une bonne conception est
>>> essentielle, quelque soit le langage.



Absolument. La phase de programmation ne doit être que la mise
en oeuvre stricte de la conception. C'est pourquoi on ne
devrait pas demander à un programmeur d'être créatif mais de
maîtriser parfaitement (j'insiste sur le parfaitement) le(s)
langage(s) dans le(s)quel(s) il programme. Dans un monde de
développement idéal, le programmeur doit être le garant que la
sémantique de l'application, décrite par le dossier
fonctionnel et la conception, est respecté dans sa
transcription à l'aide d'un langage sur la machine.



>> Je considère que c'est encore plus critique sur les
>> langages statiquement typés avec peu/pas de fonctionnalités
>> dynamiques comme C et C++ et où le manque de flexibilité du
>> design se paye au prix fort.



> C'est d'autant plus vrai pour les langages dynamiques où il
> est nécessaire d'avoir une bonne couverture de test et où
> l'architecture est pressurés par la testabilité.



Dans les applications industrielles et, en particulier, temps
réel les différentes phases de tests peuvent représenter
jusqu'à 60% du total des hommes.mois du projet. Au passage,
la couverture de test ne fait pas tout, hélas. Une couverture
C1 à 100% ne garantit absolument pas l'absence de défaut...



Certainement. C'est pourquoi on a des revues de code, par
exemple. Les tests sont plutôt une espèce de garde-fou ;
normalement, un test qui échoue est symptome d'un problème dans
le processus de développement.

>> Le minimum étant les interfaces à-la-Java que
>> malheureusement C++ n'a pas (mais pourrait).



Les interfaces sont les "contrats" des différents modules. Les
interfaces doivent comprendre les pré et post conditions sur
l'utilisations des différentes fonctions des interfaces.



Exactement.

Je ne comprends toujours pas, qu'au bout de plus de 20 ans,
C++ ne possède pas la notion d'interface.



Mais il la possède. Bien plus que Java, par exemple.

Les classes abstraites peuvent jouer partiellement ce rôle
mais c'est en trichant avec cet idiome.



Les classes abstraites en C++ permettent à définir une interface
aussi bien qu'en Eiffel, et bien mieux qu'en Java. Je le fais
depuis des années.

> Quel est le problème avec les classes virtuelles pures ?



> struct IConnectable
> {
> virtual connect()=0;
> virtual disconnect()=0;
> };



> Je dirai plutôt que c'est Java qui n'a pas voulu des
> héritages multiples.



Heureusement. L'héritage multiple (en C++, en particulier) est
une hérésie. Java n'a pas voulu retomber dans ces travers et
on comprend pourquoi car avec l'héritage multiple on peut
avoir plusieurs chemins d'héritage, d'où la notion de
dérivation virtuelle et des avatars que cela engendre...



C-à-d ? Je n'ai jamais encore travaillé sur une application
ni en C++ ni en Java sans utiliser l'héritage virtuel (ce qui en
Java s'appelle « extends », et qui est limité à des classes
sans contrat, c-à-d ce que Java appelle des interfaces).

[...]
Au moins, en Java, même si j'ai l'héritage d'interface, je
suis garantis que tant que je dis interface, il n'ya aucune
implémentation d'aucune méthode et quand je dis qu'une classe
"implements" une ou des interface, je dois définir (coder)
toutes les méthodes spécifiées de toutes les interfaces. C'est
donc beaucoup plus propre.



Tu es aussi garanti qu'il n'a pas de contrat. Ce qui est plutôt
génant, à mon avis.

De plus, en Java, je peux déclarer qu'une classe est "final"
ce qui interdit de faire des sous-classes. Cela manque
beaucoup en C++.



Là, je suis plutôt d'accord. En fait, qu'on puisse en dériver ou
non n'est pas la question. C'est qu'on ne pourrait plus
supplanter la fonction en question (qui est, très souvent toutes
les fonctions).

La possibilité de pouvoir dériver de toute classe, quelque
qu'elle soit, sert apparamment dans certains idiomes de la
métaprogrammation avec templates. D'où une résistance farouche
contre l'interdire. Mais j'aurais bien voulu la possibilité de
rendre une fonction finale, avec en plus (par mesure de
commodité) la possibilité de rendre toutes les fonctions d'une
classe finales, même si ça n'empêchait pas l'héritage.

--
James Kanze (GABI Software) email:
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
James Kanze
On Oct 30, 6:38 pm, ld wrote:
On 30 oct, 17:59, James Kanze wrote:



[...]
> > Imagine toi assister à une réunion en Mandarin (à moins
> > que tu ne le parles?), il te sera difficile de
> > participer...



> Certes, mais pour une poste en France, je m'attendrais qu'on
> discute soit en français, soit en anglais (dans le contexte
> d'un projet international). Ou sinon qu'une connaissance de
> Mandarin soit exigée d'avance, dans quel cas, je ne me
> postulerais même pas. Mais je ne vois pas trop le rapport
> avec des compétences C++.



C'est que si il ne comprend pas le C++, une discussion
technique aura l'air d'etre du chinois pour lui et sa
participation proche de zero.



Connaître les détails du langage n'est pas forcément nécessaire
pour en parler. Il suffit d'un peu de vocabulaire.

[...]
> > > Je ne sais pas. La dernière fois que j'avais à valider
> > > les compétences des candidats, je trouveais que 9 sur 10
> > > s'heurter sur des questions aussi simple que « Pourquoi
> > > est-ce qu'on déclarerait un destructeur virtuel ? ».
> > Parce que le compilateur le demande ;-)



> Justement non. Le compilateur ne le démande pas.



gcc le demande des que ta classe definit un type polymorphique.



Ah bon. Parce que moi, j'ai des cas particulier où je dérive
d'une classe sans destructeur virtuel, et g++ ne se plaint pas.
Le langage l'autorise, tant que tu ne fais pas de delete à
travers un pointeur vers la base.

> La réponse
> exacte comportera obligatoirement des mots « comportement
> indéfin ». Mais j'acceptais aussi des réponses à peu près
> correctes.



Entendre des mots comme "pure virtual call" ou "memory leak"
ou quelques mots sur la nature monomorphique de ::delete
serait evidement un plus ;-)



Pas autant que « comportement indéfini ». Mais tout indication
qu'il y avait à voir avec l'héritage aurait été bien venu.

--
James Kanze (GABI Software) email:
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
Wykaaa
Olivier Miakinen a écrit :
Le 30/10/2008 17:55, Wykaaa a écrit :
C'est bien ce qui trouble nombre de débutants en C/C++ (et même quelques
fois, des non débutants, ces espèces d'équivalences pointeurs/tableau.

D'ailleurs on peut écrire : "0123456789ABCDEF"[i].



On peut même écrire : i["0123456789ABCDEF"] !

Je peux te garantir que ceci trouble même certains programmeurs C très
expérimentés.



Cette toute dernière façon de faire est en effet assez troublante.


C'est en fait une vieille histoire car, à l'origine, K&R voulaient que
tout ce qu'on écrivait en C ait une signification.
C'est pour ça qu'on peut (pouvait ?) écrire T[i] ou i[T] !
Avatar
Olivier Miakinen
Le 30/10/2008 19:13, Wykaaa a écrit :

On peut même écrire : i["0123456789ABCDEF"] !
Cette toute dernière façon de faire est en effet assez troublante.



C'est en fait une vieille histoire car, à l'origine, K&R voulaient que
tout ce qu'on écrivait en C ait une signification.



Je ne connaissais pas la raison historique. Merci.

C'est pour ça qu'on peut (pouvait ?) écrire T[i] ou i[T] !



On peut toujours, au moins dans le GNU gcc 3.4.4.
Avatar
ld
On 30 oct, 18:51, James Kanze wrote:
On Oct 30, 4:15 pm, ld wrote:



> On 30 oct, 15:02, Michael DOUBEZ wrote:
> > ld a écrit :
> > > Le minimum étant les interfaces à-la-Java que
> > > malheureusement C++ n'a pas (mais pourrait).
> > Quel est le problème avec les classes virtuelles pures ?
> > struct IConnectable
> > {
> >    virtual connect()=0;
> >    virtual disconnect()=0;
> > };
> > Je dirai plutôt que c'est Java qui n'a pas voulu des
> > héritages multiples.
> Il y en a plusieurs:
> Les instances contiendront (n+1) pointeurs vers les multiples
> vtbls pour n heritage multiples, meme si ses classes
> abstraites de base n'ont pas de donnees membres (interfaces).
> Le modele objet de C++ pourrait optimiser ca, mais c'est assez
> complique a cause d'une part du "vrai" heritage multiple et
> d'autre part des contraintes dans les ctors/dtors. En tout cas
> gcc ne le fait pas. Donc si la classe implemente ± 10
> interfaces, un cas assez courant pour un design "flexible", on
> se retrouve avec 5 a 10 pointeurs dans les instances (je
> suppose quelques heritages simples dans le tas). Ces pointeurs
> necessitent des ajustements d'offsets dans l'instance a chaque
> fois que l'on utilise l'instance a travers une de ses
> interfaces (ce qui est le but pour la flexibilite), et ce qui
> demande de generer du code et des calculs en plus.

Et alors ? Mieux vaut que ce soit le compilateur qui s'en
occupe, et pas toi, non ?



Je n'ai jamais dis le contraire. Il est exclu que je le fasse a la
main. Ca m'a prit assez de temps dans le papier sur le modele objet de
C++ pour en etre convaincu.

Et si tu as une classe qui implémente
plusieurs interfaces, quel est l'alternatif ?



L'heritage multiple d'interface comme en Java. Aucun surcout, simple
et efficace. Mais pas de contrat possible ou de template method
pattern sans beaucoup devoir ecrire beaucoup code.

Sans l'héritage
multiple, tu es bien obligé d'y introduire autant de
passerelles. Donc, des objets supplémentaires, avec leur vptr,
etc., un niveau d'indirection en plus, ET qui sont écrit par
toi, donc beaucoup plus de travail (et plus de possibilités
d'erreur).



Il faut l'heritage multiple, mais d'interface seulement. Note qu'il
n'y a pas d'objection pratique pour que Java puisse avoir des
implementations par defaut dans ces interfaces, ce qui simplifierait
les template method pattern et les contrats.

> De plus il faut utiliser au maximum les retours contravariant
> pour garder un maximum de visibilite statique,

L'utilisation d'un retour contravariant est en général une
indication d'une mauvaise conception, ou peut-être plutôt une
paresse dans la définition de l'interface. À éviter.



Les retours contravariant ne sont pas dans les interfaces, par
definition...

C'est dans les implementations qu'on les utilise (overriding) pour
conserver un maximum de visibilite statique quand on utilise la classe
sans passer par ses interfaces.

> Enfin, en theorie, il faudrait deriver virtuellement *par
> defaut* pour un design flexible sans "bombe a retardement",

Dans le cas où une interface étend une autre, absolument. C'est
une règle de programmation à ne pas effreindre. Il serait bien
préférable que l'héritage virtuel soit le défaut, parce qu'il
marche dans prèsque tous les cas. Mais il n'est pas essentiel
quand on hérite afin d'implémenter, plutôt que pour étendre
l'interface, parce qu'à ce moment là, au moins d'utiliser un
idiome comme le modèle de template (« template method
pattern » en anglais -- rien à voir avec les templates C++),
on sait qu'on a affaire à la classe la plus dérivée, et donc que
l'héritage virtuel n'est pas nécessaire.



oui.

> mais cela rajoute encore des pointeurs des des ajustements
> d'offset. Au final tous ces calculs intermediaires
> ralentissent la resolution des appels virtuels de facon non
> negligeable. Mes mesures sur gcc 4.2 montrent que par rapport
> a un appel virtuel avec heritage simple, l'heritage multiple
> de 4 interfaces ralentit d'un facteur ±2 l'appel, et
> l'heritage virtuel d'un facteur ±4. Soit ±8 au total si on
> mixe les deux, ce qui serait l'approche correcte. Je ne pense
> pas que les autres compilateur font mieux et je trouve deja
> pas mal compte tenu de tout ce que le compilateur doit
> generer.

Et alors ? Est-ce que tu connais des applications où ça fait
une différence ?



oui, presque toutes celles sur lesquelles je travaille. C'est tout
simplement la difference entre une application qui donne un resultat
et rien du tout. Le cas le plus fameux ici, c'est le simulateur
d'accelerateur. La version (ancienne) Fortran est environ 10 fois plus
rapide que la version (recente) ecrite en C++, du coup cette derniere
n'est jamais utilisee (les temps de calculs etant deja en heures voir
en jours pour la version Fortran). L'autre probleme vient de la
complexite du code C++ que personne ne veut reprendre.

Je me suis servi parfois des graphes
d'héritage à peine croyable (à cause des contraints techniques
liés à la façon qu'on générait le code), et ça n'a jamais eu un
impact mesurable sur les performances de l'application.



Un facteur 10 n'est pas negligeable lorsque les calculs prennent des
heures, voir des jours pour les versions non polymorphique. Mais c'est
vrai que dans la plupart des application, on ne voit probablement pas
grand chose.

> La faute au "vrai" heritage multiple.

Et qu'est-ce que tu appelles un « vrai » héritage multiple ?
Et comment est-ce qu'il est implémenté ?



Je te laisse te documenter. Un point de depart possible, c'est le
papier sur le modele objet de C++ que j'avais commence en 2005 ou le
papier de Nathan Sidwell qui si je me souviens bien decrivait celui de
g++. le D&E est pas mal aussi si on lit entre les lignes.

> Maintenant de mon point de vue, qui n'engage que moi, je
> trouverais beaucoup plus utile une implementation tres
> efficace des interfaces a- la-Java en C++ (space & time) a la
> place de l'heritage multiple rarement utile et facilement
> simulable quand on a vraiment besoin.

Quand tu utilises l'héritage multiple en Java, ça fonctionne
plus ou moins comme l'héritage multiple en C++. De point de vue
pratique, quand tu « extend » une interface, c'est exactement
comme si tu héritais virtuellement de la classe en C++.



Techniquement, pas du tout. L'heritage multiple de Java peut se faire
en C assez facilement, et sans besoin de code supplementaire et avec
une excellente performance. Celui de C++, c'est exclus.

a+, ld.
Avatar
David Fleury
James Kanze a écrit :


int tab[25] = { ... }; // un tableau
int* ptr = &tab[0]; // un pointeur sur le premier élément du tableau



++ptr; // élément suivant



Mais où est le pointeur sur tableau là-dedans. Plutôt :
int (*ptr)[ 25 ] = &tab ;
, il me semble. C'est au moins ce que je m'entends par
« pointeur sur tableau ».




Oui merci pour la correction.
Avatar
Michael DOUBEZ
James Kanze a écrit :
On Oct 30, 3:02 pm, Michael DOUBEZ wrote:
ld a écrit :



On 30 oct, 10:37, James Kanze wrote:
On Oct 30, 8:41 am, ld wrote:







On 28 oct, 19:13, David Fleury wrote:


[...]







En particulier avec C++, la capacite a faire un bon design
est essentielle dans un "vrai" projet.


Ce n'est pas particulier avec C++ ; une bonne conception
est essentielle, quelque soit le langage.







Je considère que c'est encore plus critique sur les langages
statiquement typés avec peu/pas de fonctionnalités
dynamiques comme C et C++ et où le manque de flexibilité du
design se paye au prix fort.





C'est d'autant plus vrai pour les langages dynamiques où il
est nécessaire d'avoir une bonne couverture de test et où
l'architecture est pressurés par la testabilité.



Le minimum étant les interfaces à-la-Java que
malheureusement C++ n'a pas (mais pourrait).





Quel est le problème avec les classes virtuelles pures ?



struct IConnectable
{
virtual connect()=0;
virtual disconnect()=0;
};



Le problème, c'est que tu ne peux pas vérifier le contrat. Dans
les cas où il n'y a pas de contrat (les inversions d'appel, par
exemple), c'est bien, mais dans les autres, en général, l'idiome
consacré est plutôt :

class Connectable
{
public
void connect()
{
// vérifier les préconditions...
doConnect() ;
// vérifier les postconditions...
}

private:
virtual void doConnect() = 0 ;
} ;

Une interface qui impose un contrat doit avoir du code, qu'il
soit généré implicitement (comme en Eiffel) ou explicitement
(comme en C++).



Oui mais j'entendais les classes virtuelles pure comme les interfaces à
la Java.
C++ offre beaucoup plus de possibilités alors que Java enferme le codeur
dans certains idiomes. Enfin c'est ce que j'ai pensé il y a quelques
années. Ça fait un moment que je n'ai plus fait de Java.

J'ai vu quelques exemples de groovy et ça me semble une bonne approche.
Tant qu'à avoir une MV, autant tirer parti d'une approche "langage
dynamique".


Je dirai plutôt que c'est Java qui n'a pas voulu des héritages
multiples.



Dans le cas des interfaces, si. Ce qu'il n'a pas voulu, c'est
l'enforcement des contrats. Donc, de vraies interfaces typées.



C'est a mon avis l'enjeu des initiatives AOP de Java comme AspectJ: ça
permet d'injecter des pre/post condition dans les interfaces.

D'après une interview de James Gosling, le système d'interface de Java
fait partie des décisions difficile qu'il a fallut prendre.
http://www.gotw.ca/publications/c_family_interview.htm
<quote>
Gosling: There are a bunch of things that I'd do differently. There are
a number of things that I'm not entirely happy with and it's not clear
what the right answer is. I'm not really happy with the schism between
interfaces and classes; in many ways it feels like the right solution
wouldn't get in the way.
</quote>

--
Michael
Avatar
James Kanze
On Oct 30, 7:13 pm, Wykaaa wrote:
Olivier Miakinen a écrit :> Le 30/10/2008 17:55, Wykaaa a écrit :
>> C'est bien ce qui trouble nombre de débutants en C/C++ (et
>> même quelques fois, des non débutants, ces espèces
>> d'équivalences pointeurs/tableau.



>> D'ailleurs on peut écrire : "0123456789ABCDEF"[i].



> On peut même écrire : i["0123456789ABCDEF"] !



>> Je peux te garantir que ceci trouble même certains
>> programmeurs C très expérimentés.



> Cette toute dernière façon de faire est en effet assez
> troublante.



C'est en fait une vieille histoire car, à l'origine, K&R
voulaient que tout ce qu'on écrivait en C ait une
signification. C'est pour ça qu'on peut (pouvait ?) écrire
T[i] ou i[T] !



Pas du tout. C dérive de B (voir
http://www.quut.com/c/msb-on-b.html). Or B fonctionnait plutôt
comme l'assembleur : la différence entre une addition d'entiers
et une addition virgule flottant, c'est que l'instruction
(opérateur) est différente. Les données n'ont pas de type ;
elles ne sont que des mots machines. Si tu faisais a + b,
c'était l'addition des entiers ; a #+ b l'addition des
flottants, et un pointeur était un entier, jusqu'au moment que
tu lui appliquais l'opérateur * (unaire). Il n'y avait pas de
struct (il n'y avait pas dans les premiers C non plus), mais
même alors, on reconnaissait la nécessité des tableaux. Donc,
table[ 10 ;
déclarait un tableau d'11 élements, ET un mot mémoire initialisé
avec son adresse, et l'opérateur [] était défini en terms des
opérations sur ce mot. La sémantique des tableaux en C dérive
directement de ce mot ; le n'existe pas en tant que variable,
mais le nom du tableau se comporte bien comme un pointeur.

--
James Kanze (GABI Software) email:
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
James Kanze
On Oct 30, 9:04 pm, ld wrote:
On 30 oct, 18:51, James Kanze wrote:
> On Oct 30, 4:15 pm, ld wrote:



> > On 30 oct, 15:02, Michael DOUBEZ wrote:
> Et si tu as une classe qui implémente plusieurs interfaces,
> quel est l'alternatif ?



L'heritage multiple d'interface comme en Java. Aucun surcout,
simple et efficace.



La seule différence entre l'héritage multiple en Java et en C++,
c'est les mots clés, et des restrictions artificielles
introduites par Java. En gros, « extends » devient « : public
virtual » en C++, quand il s'applique à une interface,
« implements » ou « extends » sur une classe, c'est sans le
« virtual ». Mais changer les mots clés et ajouter les
restrictions ne le rendent ni plus simple, ni plus efficace. Et
je ne vois pas de « surcoût » ni dans l'un ni dans l'autre :
il coûte ce qu'il coûte, et pour parler de surcoût, il faut dire
par rapport à quoi. L'alternatif (s'il n'existait pas, comme
c'était le cas en C++ pré 1988 environ), c'est beaucoup d'objets
supplémentaire, ce qui est certainement plus cher. (Logiquement,
on pourrait arguer que l'héritage multiple n'est jamais
nécessaire, puiqu'on peut toujours arriver au but au moyen des
classes passerelle. Ce n'est qu'une optimisation. Et du temps
d'exécution, et -- ce qui est plus important -- du temps du
développeur.)

Mais pas de contrat possible ou de template method pattern
sans beaucoup devoir ecrire beaucoup code.



La possibilité du contrat, c'est justement ce qui manque en
Java ; quand j'ai développé en Java, on se servait prèsque
jamais des « interface » (au moins aux frontières entre les
composants) pour cette raison. Mais je ne vois pas trop où ça
pose un problème pour le modèle de fonction template. C'est un
cas où je ne vois pas trop l'utilité de l'héritage multiple.

> Sans l'héritage multiple, tu es bien obligé d'y introduire
> autant de passerelles. Donc, des objets supplémentaires,
> avec leur vptr, etc., un niveau d'indirection en plus, ET
> qui sont écrit par toi, donc beaucoup plus de travail (et
> plus de possibilités d'erreur).



Il faut l'heritage multiple, mais d'interface seulement.



Définis-moi ce que tu entends par « interface ». Et
explique-moi ce que tu as contre les mixins.

Note qu'il n'y a pas d'objection pratique pour que Java puisse
avoir des implementations par defaut dans ces interfaces, ce
qui simplifierait les template method pattern et les contrats.



Certainement. Il n'y a pas de raison fondamentale derrière la
distinction en Java entre « interface » et « class ». Tous
les deux sont en fait des classes, au sens du C++, avec des
restrictions arbitraires. Or, dans les contextes où ces
restrictions ont un sens, rien n'interdit de les introduire en
C++, en les enforçant au moyen des revues de code ou des
préprocesseurs. Et en gardant la possibilité d'en passer outre
dans les cas où ça se justifie.

> > De plus il faut utiliser au maximum les retours
> > contravariant pour garder un maximum de visibilite
> > statique,



> L'utilisation d'un retour contravariant est en général une
> indication d'une mauvaise conception, ou peut-être plutôt
> une paresse dans la définition de l'interface. À éviter.



Les retours contravariant ne sont pas dans les interfaces, par
definition...



Donc, ils ne servent pas.

C'est dans les implementations qu'on les utilise (overriding)
pour conserver un maximum de visibilite statique quand on
utilise la classe sans passer par ses interfaces.



Et quand est-ce qu'on se sert d'une classe sans passer par son
interface ? Je parle au niveau logique ici ; l'interface
définit ce que j'ai droit à faire avec la classe (et n'est pas
forcément une classe à part -- une classe d'implémentation peut
définir aussi une interface propre à lui). Si j'ai une fonction
qui renvoie un Derived*, c'est une autre fonction que celle qui
renvoie un Base*, avec un autre contrat. Et donc, si on
programme proprement, ne supplante pas la fonction de la classe
de base. (Évidemment, si on implémente les contrats, les
fonctions publiques, visible dans l'interface, ne sont pas
virtual. La question ne se pose donc pas.)

Dans la pratique, j'admets qu'il peut y avoir des cas assez
rares où y insister serait un peu exagérer. Mais quand même :
en dehors des cas d'inversion d'appel, où tu n'as pas de
contrat, l'idiome classique serait :

class Base // ou Interface, comme tu veux...
{
public:
Base* function()
{
// ...
return doFunction() ;
}

private:
virtual Base* doFunction() = 0 ;
} ;

class Derived : public Base
{ // ou class Implementation, si tu préfère.
public:
Derived* function() // Si on veut ; dans mon expérience,
{ // c'est rarement nécessaire.
// ...
}

private:
virtual Base* doFunction()
{
return function() ;
}
} ;

En fait, j'ai utilisé ce modèle beaucoup plus souvent pour des
raisons d'optimisation que pour des raisons de typage, par
exemple, quand la fonction publique est operator[] sur une
classe qui implémente quelque chose comme vector (où le coût
d'un appel virtuel, et surtout l'impossibilité de générer la
fonction inline que cela implique, pourrait avoir des
consequences réeles sur les performances des applications).

Sinon, typiquement, la fonction publique dans Base s'appellera
quelque chose comme getBase, et celle dans Derived getDerived.

> > mais cela rajoute encore des pointeurs des des ajustements
> > d'offset. Au final tous ces calculs intermediaires
> > ralentissent la resolution des appels virtuels de facon
> > non negligeable. Mes mesures sur gcc 4.2 montrent que par
> > rapport a un appel virtuel avec heritage simple,
> > l'heritage multiple de 4 interfaces ralentit d'un facteur
> > ±2 l'appel, et l'heritage virtuel d'un facteur ±4. Soit ±8
> > au total si on mixe les deux, ce qui serait l'approche
> > correcte. Je ne pense pas que les autres compilateur font
> > mieux et je trouve deja pas mal compte tenu de tout ce que
> > le compilateur doit generer.



> Et alors ? Est-ce que tu connais des applications où ça fait
> une différence ?



oui, presque toutes celles sur lesquelles je travaille. C'est
tout simplement la difference entre une application qui donne
un resultat et rien du tout. Le cas le plus fameux ici, c'est
le simulateur d'accelerateur. La version (ancienne) Fortran
est environ 10 fois plus rapide que la version (recente)
ecrite en C++, du coup cette derniere n'est jamais utilisee
(les temps de calculs etant deja en heures voir en jours pour
la version Fortran). L'autre probleme vient de la complexite
du code C++ que personne ne veut reprendre.



Mais qu'est-ce que tu fais dans tes fonctions ? Quasiment rien,
alors. Et alors, est-ce que le problème n'est pas plutôt du à la
virtualité en soi, qui empêche (en général) la génération
inline, et non à l'héritage multiple. Aussi, n'oublie pas que
même aujourd'hui, les compilateurs Fortran optimisent bien mieux
que ceux de C++. (Si tu es concerné par le calcul numérique, et
des performances, je conseille très fortement la lecture de
Barton et Nackman. Qui utilise intensement l'héritage multiple,
sans que ça ait un impact négatif sur les performancese.)

> Je me suis servi parfois des graphes d'héritage à peine
> croyable (à cause des contraints techniques liés à la façon
> qu'on générait le code), et ça n'a jamais eu un impact
> mesurable sur les performances de l'application.



Un facteur 10 n'est pas negligeable lorsque les calculs
prennent des heures, voir des jours pour les versions non
polymorphique. Mais c'est vrai que dans la plupart des
application, on ne voit probablement pas grand chose.



Le facteur 10, c'est dans l'appel de la fonction, tout au plus.
(Et encore, sur les machines que je connais, c'est 3 ou 4, non
10.) La question est : quel percentage du temps est-ce que ton
programme passe dans l'appel des fonctions proprement dit, par
rapport au temps qu'il passe dans l'exécution de la fonction.
Quand on utilise l'héritage et la polymorphisme, c'est prèsque
toujours pour des classes d'un niveau plus élevées, où chaque
fonction fait quelque chose de relativement complexe. Et je
sais, c'est une généralité, et il y a bien des exceptions. Mais
ce n'est pas pour rien que std::vector ne comporte aucune
fonction virtuelle, ou que Barton et Nackman utilise l'idiome
que j'ai présenté ci-dessus, pour que dans les cas critique (où
le profiler dit que ça ne va pas), ils peuvent utiliser
l'operator[] non-virtuel de la classe dérivée.

(Je dois ajouter qu'il existe des optimisateurs qui rend tout ça
plus ou moins caduque ; qui utilise les données du profiling
pour décider que dans tel ou tel cas, on appel en fait toujours
la même function dans la boucle, et qui remplace la boucle avec
deux versions, une qui a la fonction inline, et teste le type
réel avant d'entrer dans la boucle. En revanche, de tels
compilateurs ne sont pas si courant.)

> > La faute au "vrai" heritage multiple.



> Et qu'est-ce que tu appelles un « vrai » héritage multiple ?
> Et comment est-ce qu'il est implémenté ?



Je te laisse te documenter.



Comment est-ce que je peux me documenter sur ce que toi, tu
entends par un « vrai » héritage multiple ? Moi, je sais ce
que j'entends par cette proposition, et je sais comment
l'implémenter dans un compilateur C++ ou un JVM. Ce que je
démande, c'est ce que toi, tu y entends. Surtout par ce
« vrai » ; en quoi est-ce que l'héritage multiple de C++ (ou
de Java) n'est pas vrai ?

Un point de depart possible, c'est le papier sur le modele
objet de C++ que j'avais commence en 2005 ou le papier de
Nathan Sidwell qui si je me souviens bien decrivait celui de
g++.



Papiers qui se trouvent où.

le D&E est pas mal aussi si on lit entre les lignes.



Je l'ai lu il y a un certain temps. J'ai aussi discuté avec
Stroustrup une ou deux fois, et avec les auteurs du compilateur
EDG et les auteurs de Sun CC. Autant que j'ai pu comprendre,
tous, ils considèrent l'héritage multiple un C++ un « vrai »
héritage multiple. L'héritage multiple de C++ correspond aussi
au signification des mots en anglais. Ce n'est certainement pas
le seul modèle possible, mais il est aussi « vrai » que les
autres modèles, et je ne sais pas quel modèle tu veux préconcisé
comme « vrai ». (Note bien que ton « vrai » là resonne
beaucoup comme le « vrai » OO dont on entend tellement parler.
Qui est toujours celle du langage préféré de l'auteur.)

> > Maintenant de mon point de vue, qui n'engage que moi, je
> > trouverais beaucoup plus utile une implementation tres
> > efficace des interfaces a- la-Java en C++ (space & time) a la
> > place de l'heritage multiple rarement utile et facilement
> > simulable quand on a vraiment besoin.



> Quand tu utilises l'héritage multiple en Java, ça fonctionne
> plus ou moins comme l'héritage multiple en C++. De point de
> vue pratique, quand tu « extend » une interface, c'est
> exactement comme si tu héritais virtuellement de la classe
> en C++.



Techniquement, pas du tout. L'heritage multiple de Java peut
se faire en C assez facilement, et sans besoin de code
supplementaire et avec une excellente performance. Celui de
C++, c'est exclus.



Et quelle est la différence ? Il te faut bien une vtable, ou
quelque chose d'équivalent, pour chaque interface dont tu
dérives. La seule différence réele que je vois, c'est que la JVM
a deux instructions différentes : invokeinterface et
invokevirtual, selon qu'on appelle un fonction à travers une
interface ou non. Quand tu invoques à travers un pointeur à une
classe, donc, la VM ou le compilateur sait d'avance qu'il n'a
pas besoin d'ajuster le pointeur. Quand tu invoques à travers
une interface, en revanche, qu'est-ce que la VM fait de moins
que quand tu appelles une fonction C++ depuis une classe de base
virtuelle ? Ensuite, évidemment, il y a des questions
d'optimisation. Où Java a l'avantage par rapport à certains
compilateurs C++ d'avoir accès aux données propre à l'exécution.
Encore que beaucoup de compilateurs C++ aujourd'hui savent se
servir des données du profiler pour optimiser.)

--
James Kanze (GABI Software) email:
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
ld
On 31 oct, 11:07, James Kanze wrote:

Pas beaucoup de temps, alors je fais court.

Dans la pratique, j'admets qu'il peut y avoir des cas assez
rares où y insister serait un peu exagérer. Mais quand même :
en dehors des cas d'inversion d'appel,



qui est pour moi un cas _extrement_ courant (si j'utilisais C++) dans
mes programmes. C'est aussi la raison pour laquelle je ne concois pas
de revenir sur un langage qui ne supporte pas les multimethodes (et
ils ne sont pas legion)

où tu n'as pas de
contrat, l'idiome classique serait :

    class Base  // ou Interface, comme tu veux...
    {
    public:
        Base* function()
        {
            //  ...
            return doFunction() ;
        }

    private:
        virtual Base* doFunction() = 0 ;
    } ;

    class Derived : public Base
    {                   // ou class Implementation, si tu préfère.
    public:
        Derived* function()     // Si on veut ; dans mon exp érience,
        {                       // c'est ra rement nécessaire.
            // ...
        }

    private:
        virtual Base* doFunction()
        {
            return function() ;
        }
    } ;

En fait, j'ai utilisé ce modèle beaucoup plus souvent pour des
raisons d'optimisation que pour des raisons de typage,



C'est pourtant la facon classique de faire. De mon cote je prefere la
facon suivante, ca pollue moins l'interface:

struct Interface {
virtual Interface* function() = 0 ;
};

class Concrete : public virtual Interface {
Concrete* func() {
func_pre();
...
func_post();
}

protected:
void func_pre() { ... }
void func_post() { ... }

private:
virtual Interface* function() { return func(); }
};

par
exemple, quand la fonction publique est operator[] sur une
classe qui implémente quelque chose comme vector (où le coût
d'un appel virtuel, et surtout l'impossibilité de générer la
fonction inline que cela implique, pourrait avoir des
consequences réeles sur les performances des applications).



Ce n'est pas la seule raison. Ou mets-tu les contrats de la classe si
tu n'as pas de version publique non virtuelle dans ta classe?
L'avantage de ma version (surtout quand on doit l'expliquer a des
etudiants avec peu de background OO), c'est que l'Interface est
equivalente a une API C, ni plus ni moins, et que la partie publique
de la classe est equivalent a une classe monomorphique. Apres on
rajoute la colle entre l'interface et la classe qui devient
polymorphique et exportable.

Sinon, typiquement, la fonction publique dans Base s'appellera
quelque chose comme getBase, et celle dans Derived getDerived.



? J'utilise tres tres rarement des getters ou des settesr si c'est ce
dont tu parles. Je n'aime pas ca.

> > > mais cela rajoute encore des pointeurs des des ajustements
> > > d'offset. Au final tous ces calculs intermediaires
> > > ralentissent la resolution des appels virtuels de facon
> > > non negligeable. Mes mesures sur gcc 4.2 montrent que par
> > > rapport a un appel virtuel avec heritage simple,
> > > l'heritage multiple de 4 interfaces ralentit d'un facteur
> > > ±2 l'appel, et l'heritage virtuel d'un facteur ±4. Soit ±8
> > > au total si on mixe les deux, ce qui serait l'approche
> > > correcte. Je ne pense pas que les autres compilateur font
> > > mieux et je trouve deja pas mal compte tenu de tout ce que
> > > le compilateur doit generer.
> > Et alors ? Est-ce que tu connais des applications où ça fait
> > une différence ?
> oui, presque toutes celles sur lesquelles je travaille. C'est
> tout simplement la difference entre une application qui donne
> un resultat et rien du tout. Le cas le plus fameux ici, c'est
> le simulateur d'accelerateur. La version (ancienne) Fortran
> est environ 10 fois plus rapide que la version (recente)
> ecrite en C++, du coup cette derniere n'est jamais utilisee
> (les temps de calculs etant deja en heures voir en jours pour
> la version Fortran). L'autre probleme vient de la complexite
> du code C++ que personne ne veut reprendre.

Mais qu'est-ce que tu fais dans tes fonctions ? Quasiment rien,
alors.



L'unite d'encapsulation est pour moi la fonction pour avoir un maximum
de reutilisabilite et de composabilite. Ca ce traduit par de petite
d'interfaces orthogonales avec quelques fonctions qui remplissent
chacune une tache simple et claire ("a chacun sa responsabilite").
Donc le dispatch a son importance.

Et alors, est-ce que le problème n'est pas plutôt du à la
virtualité en soi, qui empêche (en général) la génération



Je considere qu'un appel virtuel en presence d'heritage simple comme
etant l'unite a payer puisque je favorise la flexibilite, donc le
polymorphisme (sinon j'utiliserais Fortan ou C). Et cote performance,
cela me convient tres bien (surtout en C++ qui est tres efficace pour
ca). Donc je ramene tout a cette rapidite "unitaire". Le design guidé
par l'inlining est tres rare sauf en cas de specialisation
particuliere apres profiling.

inline, et non à l'héritage multiple. Aussi, n'oublie pas que
même aujourd'hui, les compilateurs Fortran optimisent bien mieux
que ceux de C++. (Si tu es concerné par le calcul numérique, et
des performances, je conseille très fortement la lecture de
Barton et Nackman. Qui utilise intensement l'héritage multiple,
sans que ça ait un impact négatif sur les performancese.)



Que j'ai depuis plus de 10 ans ;-)

> > Je me suis servi parfois des graphes d'héritage à peine
> > croyable (à cause des contraints techniques liés à la façon
> > qu'on générait le code), et ça n'a jamais eu un impact
> > mesurable sur les performances de l'application.
> Un facteur 10 n'est pas negligeable lorsque les calculs
> prennent des heures, voir des jours pour les versions non
> polymorphique. Mais c'est vrai que dans la plupart des
> application, on ne voit probablement pas grand chose.

Le facteur 10, c'est dans l'appel de la fonction, tout au plus.



Ce facteur correspond a la rapidite globale du programme. Comme je
l'ai dis, je n'ai jamais mis le nez dans le code et donc jamais fait
de profiling dessus.

Comment est-ce que je peux me documenter sur ce que toi, tu
entends par un « vrai » héritage multiple ?



en quoi est-ce que l'héritage multiple de C++ (ou
de Java) n'est pas vrai ?



C++ supporte l'heritage multiple d'implementation (ce que j'ai appelle
"vrai" pour simplifier) qui demande au compilateur de faire une
contruction particuliere des instances et de generer plusieurs vtbl
(et version de contructeurs) pour une classe polymorphique avec
heritage multiple s'entend, tandis que Java supporte l'heritage
multiple d'interface qui n'est rien d'autre que la definition de sa
vtbl de complement "lineaire" et unique.

> Un point de depart possible, c'est le papier sur le modele
> objet de C++ que j'avais commence en 2005 ou le papier de
> Nathan Sidwell qui si je me souviens bien decrivait celui de
> g++.

Papiers qui se trouvent où.



Il est cite en refence dans mon papier incomplet:
http://www.codesourcery.com/public/publications/a_common_vendor_abi_for_cpl usplus.pdf

> le D&E est pas mal aussi si on lit entre les lignes.

Je l'ai lu il y a un certain temps. J'ai aussi discuté avec
Stroustrup une ou deux fois, et avec les auteurs du compilateur
EDG et les auteurs de Sun CC. Autant que j'ai pu comprendre,
tous, ils considèrent l'héritage multiple un C++ un « vrai »
héritage multiple.



C'est ce que je dis (ca tombe bien ;-).

> L'heritage multiple de Java peut
> se faire en C assez facilement, et sans besoin de code
> supplementaire et avec une excellente performance. Celui de
> C++, c'est exclus.

Et quelle est la différence ? Il te faut bien une vtable, ou
quelque chose d'équivalent, pour chaque interface dont tu
dérives.



Dans le cas de Java, elle peut etre compacte et unique.

La seule différence réele que je vois, c'est que la JVM
a deux instructions différentes : invokeinterface et
invokevirtual, selon qu'on appelle un fonction à travers une
interface ou non. Quand tu invoques à travers un pointeur à une
classe, donc, la VM ou le compilateur sait d'avance qu'il n'a
pas besoin d'ajuster le pointeur. Quand tu invoques à travers
une interface, en revanche, qu'est-ce que la VM fait de moins
que quand tu appelles une fonction C++ depuis une classe de base
virtuelle ?



tout est explique dans le papier de Nathan, le mien ou l'ABI du C++ a
la section virtual call.

 Ensuite, évidemment, il y a des questions
d'optimisation. Où Java a l'avantage par rapport à certains
compilateurs C++ d'avoir accès aux données propre à l'exécution.



Quand on utilise les interfaces, il ne peut pas faire grand chose de
plus que C++. J'en ai fait la demonstration pas plus tard que ce debut
de semaine a mon thesard (qui tres pro Java).

a+, ld.