Pourquoi le C ++ STL est-il si fortement basé sur des modèles? (et non sur * interfaces *)

211

Je veux dire, en dehors de son nom obligatoire (la bibliothèque de modèles standard) ...

C ++ avait initialement pour but de présenter les concepts de POO en C. Autrement dit: vous pouviez dire ce qu'une entité spécifique pouvait et ne pouvait pas faire (quelle que soit la façon dont elle le faisait) en fonction de sa classe et de sa hiérarchie de classes. Certaines compositions de capacités sont plus difficiles à décrire de cette manière en raison des problèmes d'héritage multiple et du fait que C ++ prend en charge le concept d'interfaces de manière quelque peu maladroite (par rapport à java, etc.), mais il est là (et pourrait être amélioré).

Et puis les modèles sont entrés en jeu, avec la STL. La STL semblait prendre les concepts OOP classiques et les vider dans le drain, en utilisant des modèles à la place.

Il devrait y avoir une distinction entre les cas où des modèles sont utilisés pour généraliser des types où les types eux-mêmes ne sont pas pertinents pour le fonctionnement du modèle (conteneurs, par exemple). Avoir un vector<int>sens est parfaitement logique.

Cependant, dans de nombreux autres cas (itérateurs et algorithmes), les types basés sur des modèles sont supposés suivre un "concept" (Input Iterator, Forward Iterator, etc ...) où les détails réels du concept sont entièrement définis par la mise en œuvre du modèle fonction / classe, et non par la classe du type utilisé avec le modèle, qui est un peu anti-utilisation de la POO.

Par exemple, vous pouvez indiquer la fonction:

void MyFunc(ForwardIterator<...> *I);

Mise à jour: Comme il n'était pas clair dans la question d'origine, ForwardIterator peut être configuré lui-même pour autoriser tout type de ForwardIterator. Le contraire est d'avoir ForwardIterator comme concept.

attend un itérateur avancé uniquement en regardant sa définition, où vous auriez besoin de regarder l'implémentation ou la documentation pour:

template <typename Type> void MyFunc(Type *I);

Deux affirmations que je peux faire en faveur de l'utilisation de modèles: le code compilé peut être rendu plus efficace, en compilant le modèle sur mesure pour chaque type utilisé, au lieu d'utiliser vtables. Et le fait que les modèles peuvent être utilisés avec des types natifs.

Cependant, je cherche une raison plus profonde pour laquelle l'abandon de la POO classique au profit des modèles pour la STL? (En supposant que vous ayez lu jusqu'ici: P)

OB OB
la source
4
Vous pouvez consulter stackoverflow.com/questions/31693/… . La réponse acceptée est une excellente explication de ce que les modèles vous offrent par rapport aux génériques.
James McMahon
6
@ Jonas: Cela n'a pas de sens. La contrainte sur le cache coûte des cycles d'horloge, c'est pourquoi elle est importante. En fin de compte, ce sont les cycles d'horloge, et non le cache, qui définissent les performances. La mémoire et le cache ne sont importants que dans la mesure où ils affectent les cycles d'horloge passés. De plus, l'expérience peut se faire facilement. Comparez, disons, std :: for_Each appelé avec un argument foncteur, avec l'approche équivalente OOP / vtable. La différence de performances est stupéfiante . C'est pourquoi la version du modèle est utilisée.
jalf
7
et il n'y a aucune raison pour que le code redondant remplisse l'icache. Si j'instancie vector <char> et vector <int> dans mon programme, pourquoi le code vector <char> devrait-il être chargé dans icache pendant que je traite le vecteur <int>? En fait, le code du vecteur <int> est coupé car il ne doit pas inclure de code pour la conversion, les vtables et l'indirection.
jalf
3
Alex Stepanov explique pourquoi l'héritage et l'égalité ne fonctionnent pas bien ensemble.
fredoverflow
6
@BerndJendrissek: Uhm, proche, mais pas toi-même. Oui, plus de code coûte en termes de bande passante mémoire et d'utilisation du cache s'il est réellement utilisé . Mais il n'y a aucune raison particulière de s'attendre à ce qu'un vector<int>et vector<char>à être utilisé en même temps. Ils pourraient, bien sûr, mais vous pouvez utiliser les deux morceaux de code en même temps. Cela n'a rien à voir avec les modèles, C ++ ou la STL. Il n'y a rien dans l'instanciation vector<int>qui nécessite vector<char>le chargement ou l'exécution de code.
jalf

Réponses:

607

La réponse courte est "parce que C ++ a évolué". Oui, à la fin des années 70, Stroustrup avait l'intention de créer un C amélioré avec des capacités OOP, mais cela fait longtemps. Au moment où la langue a été normalisée en 1998, elle n'était plus une langue OOP. C'était un langage multi-paradigme. Il avait certainement un certain support pour le code OOP, mais il avait également un langage de modèle complet complet, il permettait la métaprogrammation au moment de la compilation, et les gens avaient découvert la programmation générique. Du coup, la POO ne semblait tout simplement pas si importante. Pas quand nous pouvons écrire du code plus simple, plus concis et plus efficace en utilisant des techniques disponibles via des modèles et une programmation générique.

OOP n'est pas le Saint Graal. C'est une idée mignonne, et c'était une amélioration par rapport aux langages procéduraux dans les années 70 quand elle a été inventée. Mais honnêtement, ce n'est pas tout ce qu'il a craqué. Dans de nombreux cas, il est maladroit et verbeux et ne favorise pas vraiment le code réutilisable ou la modularité.

C'est pourquoi la communauté C ++ est aujourd'hui beaucoup plus intéressée par la programmation générique, et pourquoi tout le monde commence enfin à réaliser que la programmation fonctionnelle est également assez intelligente. La POO en elle-même n'est tout simplement pas une jolie vue.

Essayez de dessiner un graphique de dépendance d'une STL hypothétique "OOP-ified". Combien de classes devraient se connaître? Il y aurait beaucoup de dépendances. Seriez-vous en mesure d'inclure uniquement l'en- vectortête, sans également obtenir iteratorou même iostreamtiré? La STL rend cela facile. Un vecteur connaît le type d'itérateur qu'il définit, et c'est tout. Les algorithmes STL ne savent rien . Ils n'ont même pas besoin d'inclure un en-tête d'itérateur, même s'ils acceptent tous les itérateurs comme paramètres. Quel est le plus modulaire alors?

La STL peut ne pas suivre les règles de la POO telle que Java la définit, mais n'atteint-elle pas les objectifs de la POO? N'at-il pas atteint la réutilisabilité, le faible couplage, la modularité et l'encapsulation?

Et cela n'atteint-il pas ces objectifs mieux qu'une version OOP?

Quant à savoir pourquoi le STL a été adopté dans la langue, plusieurs choses se sont produites qui ont conduit au STL.

Tout d'abord, des modèles ont été ajoutés à C ++. Ils ont été ajoutés pour la même raison que les génériques ont été ajoutés à .NET. Cela semblait une bonne idée de pouvoir écrire des trucs comme des «conteneurs de type T» sans jeter la sécurité de type. Bien sûr, la mise en œuvre sur laquelle ils se sont installés était beaucoup plus complexe et puissante.

Ensuite, les gens ont découvert que le mécanisme de modèle qu'ils avaient ajouté était encore plus puissant que prévu. Et quelqu'un a commencé à expérimenter l'utilisation de modèles pour écrire une bibliothèque plus générique. Un inspiré par la programmation fonctionnelle et un qui a utilisé toutes les nouvelles capacités de C ++.

Il l'a présenté au comité du langage C ++, qui a mis un certain temps à s'y habituer car il avait l'air si étrange et différent, mais a finalement réalisé qu'il fonctionnait mieux que les équivalents OOP traditionnels qu'ils auraient dû inclure autrement . Ils y ont donc apporté quelques ajustements et l'ont adopté dans la bibliothèque standard.

Ce n'était pas un choix idéologique, ce n'était pas un choix politique de "voulons-nous être OOP ou non", mais un choix très pragmatique. Ils ont évalué la bibliothèque et constaté qu'elle fonctionnait très bien.

Dans tous les cas, les deux raisons que vous mentionnez pour favoriser la STL sont absolument essentielles.

La bibliothèque standard C ++ doit être efficace. S'il est moins efficace que, disons, le code C équivalent roulé à la main, alors les gens ne l'utiliseront pas. Cela réduirait la productivité, augmenterait la probabilité de bugs et serait globalement une mauvaise idée.

Et la STL doit fonctionner avec les types primitifs, car les types primitifs sont tout ce que vous avez en C, et ils sont une partie majeure des deux langages. Si la STL ne fonctionnait pas avec des tableaux natifs, ce serait inutile .

Votre question part du principe que la POO est "la meilleure". Je suis curieux de savoir pourquoi. Vous demandez pourquoi ils "ont abandonné la POO classique". Je me demande pourquoi ils auraient dû s'en tenir à ça. Quels avantages aurait-il eu?

jalf
la source
22
C'est un bon résumé, mais j'aimerais souligner un détail. La STL n'est pas un "produit" de C ++. En fait, STL, en tant que concept, existait avant C ++, et C ++ se trouvait être un langage efficace ayant (presque) assez de puissance pour la programmation générique, donc STL a été écrit en C ++.
Igor Krivokon
17
Étant donné que les commentaires continuent de le faire apparaître, oui, je sais que le nom STL est ambigu. Mais je ne peux pas penser à un meilleur nom pour "la partie de la bibliothèque standard C ++ qui est modelée sur la STL". Le nom de facto pour cette partie de la bibliothèque standard est juste "la STL", même s'il est strictement inexact. :) Tant que les gens n'utilisent pas STL comme nom pour l' ensemble de la bibliothèque standard (y compris IOStreams et les en-têtes C stdlib), je suis content. :)
2010
5
@einpoklum Et que gagneriez-vous exactement d'une classe de base abstraite? Prenons std::setl'exemple. Il n'hérite pas d'une classe de base abstraite. Comment cela limite-t-il votre utilisation std::set? Y a-t-il quelque chose que vous ne pouvez pas faire avec un std::setcar il n'hérite pas d'une classe de base abstraite?
fredoverflow
22
@einpoklum veuillez jeter un œil au langage Smalltalk, qu'Alan Kay a conçu pour être un langage OOP lorsqu'il a inventé le terme OOP. Il n'avait pas d'interfaces. La POO ne concerne pas les interfaces ou les classes de base abstraites. Est -ce que vous allez dire que « Java, qui ne ressemble en rien ce que l'inventeur du terme POO avait à l' esprit est plus POO que C ++ , qui aussi est rien comme ce que l'inventeur du terme POO avait à l' esprit »? Ce que vous voulez dire, c'est que "C ++ n'est pas assez semblable à Java à mon goût". C'est juste, mais cela n'a rien à voir avec la POO.
2014
8
@MasonWheeler si cette réponse était un tas de bêtises flagrantes, vous ne verriez pas littéralement des centaines de développeurs dans le monde voter +1 à ce sujet avec seulement trois personnes faisant autrement
panda-34
88

La réponse la plus directe à ce que je pense que vous demandez / vous plaignez est la suivante: l'hypothèse que C ++ est un langage OOP est une fausse hypothèse.

C ++ est un langage multi-paradigme. Il peut être programmé en utilisant les principes de la POO, il peut être programmé de manière procédurale, il peut être programmé de manière générique (modèles), et avec C ++ 11 (anciennement appelé C ++ 0x) certaines choses peuvent même être programmées de manière fonctionnelle.

Les concepteurs de C ++ voient cela comme un avantage, ils diraient donc que contraindre C ++ à agir comme un langage purement POO lorsque la programmation générique résout mieux le problème et, enfin, de manière plus générique , serait un pas en arrière.

Tyler McHenry
la source
4
"et avec C ++ 0x certaines choses peuvent même être programmées fonctionnellement" - il peut être programmé fonctionnellement sans ces fonctionnalités, juste plus verbeusement.
Jonas Kölker
3
@Tyler En effet, si vous contraigniez C ++ à la POO pure, vous vous retrouveriez avec Objective-C.
Justicle
@TylerMcHenry: Après avoir posé cette question , je trouve que je viens de prononcer la même réponse que vous! Un seul point. J'aimerais que vous ajoutiez le fait que la bibliothèque standard ne peut pas être utilisée pour écrire du code orienté objet.
einpoklum
74

D'après ce que je comprends, Stroustrup préférait à l'origine une conception de conteneur "de style OOP" et, en fait, ne voyait pas d'autre moyen de le faire. Alexander Stepanov est le responsable de la STL, et ses objectifs n'incluaient pas "le rendre orienté objet" :

C'est le point fondamental: les algorithmes sont définis sur des structures algébriques. Il m'a fallu encore quelques années pour réaliser qu'il faut étendre la notion de structure en ajoutant des exigences de complexité aux axiomes réguliers. ... Je crois que les théories des itérateurs sont aussi centrales en informatique que les théories des anneaux ou des espaces de Banach sont centrales en mathématiques. Chaque fois que je regardais un algorithme, j'essayais de trouver une structure sur laquelle il était défini. Donc, ce que je voulais faire, c'était décrire les algorithmes de manière générique. Voilà ce que j'aime faire. Je peux passer un mois à travailler sur un algorithme bien connu pour essayer de trouver sa représentation générique. ...

STL, du moins pour moi, représente le seul moyen de programmation possible. Il est, en effet, assez différent de la programmation C ++ tel qu'il a été présenté et est toujours présenté dans la plupart des manuels. Mais, vous voyez, je n'essayais pas de programmer en C ++, j'essayais de trouver la bonne façon de gérer les logiciels. ...

J'ai eu de nombreux faux départs. Par exemple, j'ai passé des années à essayer de trouver une utilisation pour l'héritage et les virtuels, avant de comprendre pourquoi ce mécanisme était fondamentalement défectueux et ne devait pas être utilisé. Je suis très heureux que personne ne puisse voir toutes les étapes intermédiaires - la plupart d'entre elles étaient très stupides.

(Il explique pourquoi l'héritage et les virtuels - alias la conception orientée objet "était fondamentalement défectueux et ne devrait pas être utilisé" dans le reste de l'interview).

Une fois que Stepanov a présenté sa bibliothèque à Stroustrup, Stroustrup et d'autres ont fait des efforts herculéens pour l'intégrer dans la norme ISO C ++ (même interview):

Le soutien de Bjarne Stroustrup a été crucial. Bjarne voulait vraiment STL dans la norme et si Bjarne veut quelque chose, il l'obtient. ... Il m'a même forcé à faire des changements dans STL que je ne ferais jamais pour personne d'autre ... c'est la personne la plus simple d'esprit que je connaisse. Il fait avancer les choses. Il lui a fallu un certain temps pour comprendre en quoi consistait STL, mais quand il l'a fait, il était prêt à passer au travers. Il a également contribué à STL en défendant le point de vue selon lequel plus d'une façon de programmation était valable - sans fin de flak et de battage publicitaire pendant plus d'une décennie, et en poursuivant une combinaison de flexibilité, d'efficacité, de surcharge et de sécurité de type dans modèles qui ont rendu STL possible. Je voudrais dire très clairement que Bjarne est le concepteur de langage par excellence de ma génération.

Max Lybbert
la source
2
Entretien intéressant. Je suis presque sûr de l'avoir lu il y a quelque temps, mais ça valait vraiment la peine d'être revu. :)
jalf
3
L'une des interviews les plus intéressantes sur la programmation que j'ai jamais lues. Bien que cela me donne soif de plus de détails ...
Felixyz
Beaucoup de plaintes qu'il fait au sujet de langages comme Java ("Vous ne pouvez pas écrire un max générique () en Java qui prend deux arguments d'un certain type et a une valeur de retour de ce même type") ne concernaient que les premières versions de la langue, avant l'ajout de génériques. Même depuis le début, il était connu que des génériques seraient éventuellement ajoutés, bien que (une fois qu'une syntaxe / sémantique viable ait été déterminée), ses critiques sont donc largement sans fondement. Oui, les génériques sous une forme ou une autre sont nécessaires pour préserver la sécurité des types dans un langage typé statiquement, mais non, cela ne rend pas OO sans valeur.
Some Guy
1
@SomeGuy Ce ne sont pas des plaintes concernant Java en soi. Il parle de " la programmation OO" standard "de SmallTalk ou, disons, Java ". L'interview date de la fin des années 90 (il mentionne travailler chez SGI, qu'il a quitté en 2000 pour travailler chez AT&T). Les génériques n'ont été ajoutés à Java qu'en 2004 dans la version 1.5 et ils sont une déviation du modèle OO "standard".
melpomene
24

La réponse se trouve dans cet entretien avec Stepanov, l'auteur de la STL:

Oui. STL n'est pas orienté objet. Je pense que l'orientation orientée objet est presque autant un canular que l'intelligence artificielle. Je n'ai pas encore vu un morceau de code intéressant qui vient de ces gens OO.

StackedCrooked
la source
Joli bijou; Tu sais de quelle année ça vient?
Kos
2
@Kos, selon le web.archive.org/web/20000607205939/http://www.stlport.org/… la première version de la page liée date du 7 juin 2001. La page elle-même en bas indique Copyright 2001- 2008.
alfC
@Kos Stepanov mentionne travailler chez SGI dans la première réponse. Il a quitté SGI en mai 2000, donc probablement l'interview est plus ancienne que cela.
melpomene
18

Pourquoi une conception OOP pure à une bibliothèque de structure de données et d'algorithmes serait mieux?! La POO n'est pas la solution pour tout.

À mon humble avis, STL est la bibliothèque la plus élégante que j'ai jamais vue :)

pour votre question,

vous n'avez pas besoin de polymorphisme d'exécution, c'est un avantage pour STL d'implémenter la bibliothèque en utilisant le polymorphisme statique, ce qui signifie efficacité. Essayez d'écrire un tri ou une distance générique ou quel que soit l'algorithme qui s'applique à TOUS les conteneurs! votre tri en Java appelle des fonctions dynamiques à n niveaux à exécuter!

Vous avez besoin de choses stupides comme la boxe et le déballage pour cacher les suppositions désagréables des soi-disant langages Pure OOP.

Le seul problème que je vois avec STL, et les modèles en général, ce sont les terribles messages d'erreur. Ce qui sera résolu en utilisant Concepts en C ++ 0X.

Comparer STL aux collections en Java, c'est comme comparer le Taj Mahal à ma maison :)

AraK
la source
12
Quoi, le Taj Mahal est petit et élégant, et votre maison a la taille d'une montagne et un désordre complet? ;)
jalf
Les concepts ne font plus partie de c ++ 0x. Certains des messages d'erreur peuvent être anticipés en utilisant static_assertpeut - être.
KitsuneYMG
GCC 4.6 a amélioré les messages d'erreur de modèle, et je pense que 4.7+ est encore mieux avec.
David Stone
Un Concept est essentiellement "l'interface" que le PO demandait. La seule différence est que "l'héritage" d'un Concept est implicite (si une classe a toutes les fonctions membres appropriées, c'est automatiquement un sous-type du Concept) plutôt qu'explicite (une classe Java doit déclarer explicitement qu'elle implémente une interface) . Cependant, les sous-types implicites et explicites sont des OO valides, et certains langages OO ont un héritage implicite qui fonctionne exactement comme Concepts. Donc, ce qui est dit ici est essentiellement "OO craint: utilisez des modèles. Mais les modèles ont des problèmes, alors utilisez Concepts (qui sont OO)."
Some Guy
11

les types basés sur des modèles sont supposés suivre un "concept" (Input Iterator, Forward Iterator, etc ...) où les détails réels du concept sont entièrement définis par la mise en œuvre de la fonction / classe modèle, et non par la classe du type utilisé avec le modèle, qui est un peu anti-utilisation de la POO.

Je pense que vous comprenez mal l'utilisation prévue des concepts par les modèles. L'itérateur avancé, par exemple, est un concept très bien défini. Pour trouver les expressions qui doivent être valides pour qu'une classe soit un itérateur avancé, et leur sémantique, y compris la complexité de calcul, consultez la norme ou http://www.sgi.com/tech/stl/ForwardIterator.html (vous devez suivre les liens vers Input, Output et Trivial Iterator pour tout voir).

Ce document est une interface parfaitement bonne, et "les détails réels du concept" sont définis juste là. Ils ne sont pas définis par les implémentations des itérateurs avancés, et ils ne sont pas non plus définis par les algorithmes qui utilisent les itérateurs avancés.

Les différences dans la gestion des interfaces entre STL et Java sont de trois ordres:

1) STL définit des expressions valides en utilisant l'objet, tandis que Java définit des méthodes qui doivent être appelables sur l'objet. Bien sûr, une expression valide peut être un appel de méthode (fonction membre), mais ce n'est pas obligatoire.

2) Les interfaces Java sont des objets d'exécution, tandis que les concepts STL ne sont pas visibles à l'exécution même avec RTTI.

3) Si vous ne parvenez pas à valider les expressions valides requises pour un concept STL, vous obtenez une erreur de compilation non spécifiée lorsque vous instanciez un modèle avec le type. Si vous ne parvenez pas à implémenter une méthode requise d'une interface Java, vous obtenez une erreur de compilation spécifique le disant.

Cette troisième partie est si vous aimez une sorte de "typage du canard" (à la compilation): les interfaces peuvent être implicites. En Java, les interfaces sont quelque peu explicites: une classe "est" Iterable si et seulement si elle dit qu'elle implémente Iterable. Le compilateur peut vérifier que les signatures de ses méthodes sont toutes présentes et correctes, mais la sémantique est toujours implicite (c'est-à-dire qu'elles sont documentées ou non, mais seulement plus de code (tests unitaires) peut vous dire si l'implémentation est correcte).

En C ++, comme en Python, la sémantique et la syntaxe sont implicites, bien qu'en C ++ (et en Python si vous obtenez le préprocesseur de typage fort), vous obtenez de l'aide du compilateur. Si un programmeur requiert une déclaration explicite d'interfaces de type Java par la classe d'implémentation, l'approche standard consiste à utiliser des traits de type (et l'héritage multiple peut empêcher que cela soit trop verbeux). Ce qui manque, par rapport à Java, c'est un modèle unique que je peux instancier avec mon type, et qui se compilera si et seulement si toutes les expressions requises sont valides pour mon type. Cela me dirait si j'ai implémenté tous les bits requis, "avant de l'utiliser". C'est une commodité, mais ce n'est pas le cœur de la POO (et il ne teste toujours pas la sémantique,

STL peut ou non être suffisamment OO à votre goût, mais il sépare certainement l'interface proprement de l'implémentation. Il n'a pas la capacité de Java à réfléchir sur les interfaces, et il signale les violations des exigences d'interface différemment.

vous pouvez dire que la fonction ... attend un itérateur avancé uniquement en regardant sa définition, où vous auriez besoin de regarder l'implémentation ou la documentation pour ...

Personnellement, je pense que les types implicites sont une force lorsqu'ils sont utilisés de manière appropriée. L'algorithme dit ce qu'il fait avec ses paramètres de modèle, et l'implémenteur s'assure que ces choses fonctionnent: c'est exactement le dénominateur commun de ce que les "interfaces" devraient faire. De plus, avec STL, il est peu probable que vous utilisiez, par exemple, la std::copyrecherche de sa déclaration directe dans un fichier d'en-tête. Les programmeurs devraient déterminer ce qu'une fonction prend en fonction de sa documentation, et pas seulement de la signature de la fonction. Cela est vrai en C ++, Python ou Java. Il y a des limites à ce qui peut être réalisé avec la frappe dans n'importe quelle langue, et essayer d'utiliser la frappe pour faire quelque chose qu'il ne fait pas (vérifier la sémantique) serait une erreur.

Cela dit, les algorithmes STL nomment généralement leurs paramètres de modèle d'une manière qui indique clairement le concept requis. Cependant, il s'agit de fournir des informations supplémentaires utiles dans la première ligne de la documentation, et non de rendre les déclarations avancées plus informatives. Il y a plus de choses que vous devez savoir que ce qui peut être encapsulé dans les types de paramètres, vous devez donc lire les documents. (Par exemple, dans les algorithmes qui prennent une plage d'entrée et un itérateur de sortie, il est probable que l'itérateur de sortie ait besoin de suffisamment "d'espace" pour un certain nombre de sorties en fonction de la taille de la plage d'entrée et peut-être des valeurs qui s'y trouvent. Essayez de taper fortement cela. )

Voici Bjarne sur les interfaces explicitement déclarées: http://www.artima.com/cppsource/cpp0xP.html

En générique, un argument doit être d'une classe dérivée d'une interface (l'équivalent C ++ de l'interface est une classe abstraite) spécifiée dans la définition du générique. Cela signifie que tous les types d'arguments génériques doivent tenir dans une hiérarchie. Cela impose des contraintes inutiles aux conceptions et nécessite une prévoyance déraisonnable de la part des développeurs. Par exemple, si vous écrivez un générique et que je définis une classe, les gens ne peuvent pas utiliser ma classe comme argument de votre générique à moins que je ne connaisse l'interface que vous avez spécifiée et que j'en ai dérivé ma classe. C'est rigide.

En regardant l'inverse, avec la frappe de canard, vous pouvez implémenter une interface sans savoir que l'interface existe. Ou quelqu'un peut écrire une interface délibérément de telle sorte que votre classe l'implémente, après avoir consulté vos documents pour voir qu'ils ne demandent rien de ce que vous ne faites pas déjà. C'est flexible.

Steve Jessop
la source
Sur les interfaces explicitement déclarées, deux mots: classes de type. (Qui sont déjà ce que Stepanov entend par "concept".)
pyon
"Si vous ne parvenez pas à rendre valides les expressions valides requises pour un concept STL, vous obtenez une erreur de compilation non spécifiée lorsque vous instanciez un modèle avec le type." - c'est faux. Passer quelque chose à la stdbibliothèque qui ne correspond pas à un concept est généralement "mal formé, aucun diagnostic requis".
Yakk - Adam Nevraumont
Certes, je jouais vite et librement avec le terme "valide". Je voulais juste dire que si le compilateur ne peut pas compiler l'une des expressions requises, il signalera quelque chose.
Steve Jessop
8

"Pour moi, la POO signifie uniquement la messagerie, la conservation et la protection locales et la dissimulation du processus d'état, et la liaison tardive extrême de toutes choses. Cela peut être fait dans Smalltalk et dans LISP. Il existe peut-être d'autres systèmes dans lesquels cela est possible, mais Je ne les connais pas. " - Alan Kay, créateur de Smalltalk.

C ++, Java et la plupart des autres langages sont tous assez éloignés de la POO classique. Cela dit, défendre les idéologies n'est pas terriblement productif. Le C ++ n'est en aucun cas pur, il implémente donc des fonctionnalités qui semblent avoir un sens pragmatique à l'époque.

Ben Hughes
la source
7

STL a commencé avec l'intention de fournir une grande bibliothèque couvrant l'algorithme le plus couramment utilisé - avec pour objectif un comportement et des performances cohérents . Le modèle est devenu un facteur clé pour rendre cette mise en œuvre et cet objectif réalisables.

Juste pour fournir une autre référence:

Al Stevens interviewe Alex Stepanov, en mars 1995, de DDJ:

Stepanov a expliqué son expérience de travail et le choix fait vers une grande bibliothèque d'algorithmes, qui a finalement évolué en STL.

Parlez-nous de votre intérêt à long terme pour la programmation générique

..... Ensuite, on m'a proposé un emploi aux Laboratoires Bell travaillant dans le groupe C ++ sur les bibliothèques C ++. Ils m'ont demandé si je pouvais le faire en C ++. Bien sûr, je ne connaissais pas le C ++ et, bien sûr, j'ai dit que je pouvais. Mais je ne pouvais pas le faire en C ++, car en 1987 C ++ n'avait pas de modèles, qui sont essentiels pour activer ce style de programmation. L'hérédité était le seul mécanisme permettant d'obtenir la généricité et elle n'était pas suffisante.

Même maintenant, l'héritage C ++ n'est pas très utile pour la programmation générique. Voyons pourquoi. De nombreuses personnes ont tenté d'utiliser l'héritage pour implémenter des structures de données et des classes de conteneurs. Comme nous le savons maintenant, il y a eu peu ou pas de tentatives réussies. L'héritage C ++ et le style de programmation qui lui est associé sont considérablement limités. Il est impossible de mettre en œuvre une conception qui inclut une chose aussi banale que l'égalité en l'utilisant. Si vous commencez avec une classe de base X à la racine de votre hiérarchie et définissez un opérateur d'égalité virtuel sur cette classe qui prend un argument de type X, puis dérivez la classe Y de la classe X. Quelle est l'interface de l'égalité? Il a l'égalité qui compare Y à X. En utilisant les animaux comme exemple (les gens OO aiment les animaux), définissez le mammifère et dérivez la girafe du mammifère. Ensuite, définissez une fonction membre mate, où l'animal s'accouple avec l'animal et retourne un animal. Ensuite, vous dérivez la girafe de l'animal et, bien sûr, il a une fonction compagnon où la girafe s'accouple avec l'animal et retourne un animal. Ce n'est certainement pas ce que vous voulez. Bien que l'accouplement ne soit pas très important pour les programmeurs C ++, l'égalité l'est. Je ne connais pas d'algorithme unique où l'égalité d'aucune sorte n'est pas utilisée.

yowkee
la source
5

Le problème de base avec

void MyFunc(ForwardIterator *I);

comment obtenir en toute sécurité le type de chose que l'itérateur retourne? Avec les modèles, cela est fait pour vous au moment de la compilation.


la source
1
Eh bien, moi non plus: 1. N'essayez pas de l'obtenir, car j'écris du code générique. Ou, 2. Obtenez-le en utilisant le mécanisme de réflexion que C ++ propose de nos jours.
einpoklum
2

Considérons un instant la bibliothèque standard comme une base de données de collections et d'algorithmes.

Si vous avez étudié l'histoire des bases de données, vous savez sans aucun doute qu'au début, les bases de données étaient pour la plupart "hiérarchiques". Les bases de données hiérarchiques correspondaient très étroitement à la POO classique - en particulier, la variété à héritage unique, telle que celle utilisée par Smalltalk.

Au fil du temps, il est devenu évident que les bases de données hiérarchiques pouvaient être utilisées pour modéliser presque n'importe quoi, mais dans certains cas, le modèle d'héritage unique était assez limitatif. Si vous aviez une porte en bois, c'était pratique de pouvoir la regarder soit comme une porte, soit comme un morceau de matière première (acier, bois, etc.)

Ils ont donc inventé des bases de données de modèles de réseaux. Les bases de données de modèles de réseau correspondent très étroitement à l'héritage multiple. C ++ prend complètement en charge l'héritage multiple, tandis que Java prend en charge une forme limitée (vous pouvez hériter d'une seule classe, mais vous pouvez également implémenter autant d'interfaces que vous le souhaitez).

Les bases de données du modèle hiérarchique et du modèle de réseau ont pour la plupart disparu de l'usage général (bien que quelques-unes restent dans des niches assez spécifiques). Dans la plupart des cas, ils ont été remplacés par des bases de données relationnelles.

Une grande partie de la raison pour laquelle les bases de données relationnelles ont pris le dessus était la polyvalence. Le modèle relationnel est fonctionnellement un surensemble du modèle de réseau (qui est à son tour un surensemble du modèle hiérarchique).

C ++ a largement suivi le même chemin. La correspondance entre l'héritage unique et le modèle hiérarchique et entre l'héritage multiple et le modèle de réseau est assez évidente. La correspondance entre les modèles C ++ et le modèle hiérarchique est peut-être moins évidente, mais c'est quand même assez proche.

Je n'en ai pas vu de preuve formelle, mais je crois que les capacités des modèles sont un sur-ensemble de celles fournies par l'héritage multiple (qui est clairement un sur-ensemble de l'héritage unique). La partie délicate est que les modèles sont pour la plupart liés statiquement - c'est-à-dire que toutes les liaisons se produisent au moment de la compilation, et non au moment de l'exécution. En tant que tel, une preuve formelle que l'héritage fournit un surensemble des capacités d'héritage peut être quelque peu difficile et complexe (voire impossible).

Dans tous les cas, je pense que c'est la principale raison pour laquelle C ++ n'utilise pas l'héritage pour ses conteneurs - il n'y a aucune vraie raison de le faire, car l'héritage ne fournit qu'un sous-ensemble des capacités fournies par les modèles. Étant donné que les modèles sont fondamentalement une nécessité dans certains cas, ils pourraient tout aussi bien être utilisés presque partout.

Jerry Coffin
la source
0

Comment faites-vous des comparaisons avec les ForwardIterator *? Autrement dit, comment vérifiez-vous si l'article que vous avez correspond à ce que vous recherchez ou si vous l'avez ignoré?

La plupart du temps, j'utilisais quelque chose comme ça:

void MyFunc(ForwardIterator<MyType>& i)

ce qui signifie que je sais que je pointe vers MyType, et je sais comment les comparer. Bien qu'il ressemble à un modèle, il ne l'est pas vraiment (pas de mot-clé "modèle").

Tanktalus
la source
vous pouvez simplement utiliser les opérateurs <,> et = du type et ne pas savoir ce que ceux-ci (bien que ce ne soit pas ce que vous vouliez dire)
lhahne
Selon le contexte, cela peut ne pas avoir de sens ou bien fonctionner. Difficile à dire sans en savoir plus sur MyType, ce que, vraisemblablement, l'utilisateur fait, et nous non.
Tanktalus
0

Cette question a beaucoup de bonnes réponses. Il convient également de mentionner que les modèles prennent en charge une conception ouverte. Avec l'état actuel des langages de programmation orientés objet, il faut utiliser le modèle de visiteur pour traiter de tels problèmes, et la vraie POO devrait prendre en charge la liaison dynamique multiple. Voir Open Multi-Methods for C ++, P. Pirkelbauer, et.al.pour une lecture très intéressante.

Un autre point intéressant des modèles est qu'ils peuvent également être utilisés pour le polymorphisme d'exécution. Par exemple

template<class Value,class T>
Value euler_fwd(size_t N,double t_0,double t_end,Value y_0,const T& func)
    {
    auto dt=(t_end-t_0)/N;
    for(size_t k=0;k<N;++k)
        {y_0+=func(t_0 + k*dt,y_0)*dt;}
    return y_0;
    }

Notez que cette fonction fonctionnera également si Valueest un vecteur quelconque ( pas std :: vector, qui devrait être appeléstd::dynamic_array pour éviter toute confusion)

Si elle funcest petite, cette fonction gagnera beaucoup à être intégrée. Exemple d'utilisation

auto result=euler_fwd(10000,0.0,1.0,1.0,[](double x,double y)
    {return y;});

Dans ce cas, vous devez connaître la réponse exacte (2.718 ...), mais il est facile de construire un ODE simple sans solution élémentaire (Astuce: utilisez un polynôme en y).

Maintenant, vous avez une grande expression dans func, et vous utilisez le solveur ODE à de nombreux endroits, donc votre exécutable est pollué avec des instanciations de modèle partout. Que faire? La première chose à noter est qu'un pointeur de fonction régulière fonctionne. Ensuite, vous souhaitez ajouter le curry afin d'écrire une interface et une instanciation explicite

class OdeFunction
    {
    public:
        virtual double operator()(double t,double y) const=0;
    };

template
double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction& func);

Mais l'instanciation ci-dessus ne fonctionne que pour double, pourquoi ne pas écrire l'interface comme modèle:

template<class Value=double>
class OdeFunction
    {
    public:
        virtual Value operator()(double t,const Value& y) const=0;
    };

et se spécialisent pour certains types de valeur courants:

template double euler_fwd(size_t N,double t_0,double t_end,double y_0,const OdeFunction<double>& func);

template vec4_t<double> euler_fwd(size_t N,double t_0,double t_end,vec4_t<double> y_0,const OdeFunction< vec4_t<double> >& func); // (Native AVX vector with four components)

template vec8_t<float> euler_fwd(size_t N,double t_0,double t_end,vec8_t<float> y_0,const OdeFunction< vec8_t<float> >& func); // (Native AVX vector with 8 components)

template Vector<double> euler_fwd(size_t N,double t_0,double t_end,Vector<double> y_0,const OdeFunction< Vector<double> >& func); // (A N-dimensional real vector, *not* `std::vector`, see above)

Si la fonction avait d'abord été conçue autour d'une interface, alors vous auriez été obligé d'hériter de cet ABC. Vous avez maintenant cette option, ainsi que le pointeur de fonction, lambda ou tout autre objet de fonction. La clé ici est que nous devons avoir operator()(), et nous devons être en mesure d'utiliser des opérateurs arithmétiques sur son type de retour. Ainsi, la machinerie du modèle se briserait dans ce cas si C ++ n'avait pas de surcharge d'opérateur.

user877329
la source
-1

Le concept de séparer l'interface de l'interface et de pouvoir échanger les implémentations n'est pas intrinsèque à la programmation orientée objet. Je pense que c'est une idée qui a vu le jour dans le développement basé sur les composants comme Microsoft COM. (Voir ma réponse sur Qu'est-ce que le développement piloté par composants?) En grandissant et en apprenant le C ++, les gens ont été exagérés de l'héritage et du polymorphisme. Ce n'est que dans les années 90 que les gens ont commencé à dire "Programmer vers une" interface ", pas une" implémentation "" et "Favoriser" la composition des objets "plutôt que" l'héritage de classe "." (tous deux cités par GoF d'ailleurs).

Ensuite, Java est arrivé avec un récupérateur de place et un interfacemot - clé intégrés , et tout d'un coup, il est devenu pratique de séparer l'interface et la mise en œuvre. Avant que vous ne le sachiez, l'idée est devenue partie intégrante de l'OO. C ++, les modèles et STL sont antérieurs à tout cela.

Eugene Yokota
la source
Convenu que les interfaces ne sont pas seulement OO. Mais le polymorphisme de capacité dans le système de type est (c'était dans Simula dans les années 60). Les interfaces de module existaient dans Modula-2 et Ada, mais celles-ci fonctionnaient différemment dans le système de type, je pense.
andygavin