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)
vector<int>
etvector<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'instanciationvector<int>
qui nécessitevector<char>
le chargement ou l'exécution de code.Réponses:
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-
vector
tête, sans également obteniriterator
ou mêmeiostream
tiré? 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?
la source
std::set
l'exemple. Il n'hérite pas d'une classe de base abstraite. Comment cela limite-t-il votre utilisationstd::set
? Y a-t-il quelque chose que vous ne pouvez pas faire avec unstd::set
car il n'hérite pas d'une classe de base abstraite?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.
la source
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" :
(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):
la source
La réponse se trouve dans cet entretien avec Stepanov, l'auteur de la STL:
la source
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 :)
la source
static_assert
peut - être.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.
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::copy
recherche 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 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.
la source
std
bibliothèque qui ne correspond pas à un concept est généralement "mal formé, aucun diagnostic requis"."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.
la source
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.
la source
Le problème de base avec
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
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.
la source
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:
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").
la source
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
Notez que cette fonction fonctionnera également si
Value
est un vecteur quelconque ( pas std :: vector, qui devrait être appeléstd::dynamic_array
pour éviter toute confusion)Si elle
func
est petite, cette fonction gagnera beaucoup à être intégrée. Exemple d'utilisationDans 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 expliciteMais l'instanciation ci-dessus ne fonctionne que pour
double
, pourquoi ne pas écrire l'interface comme modèle:et se spécialisent pour certains types de valeur courants:
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.la source
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
interface
mot - 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.la source