Warum basiert die C ++ STL so stark auf Vorlagen? (und nicht auf * Schnittstellen *)

211

Ich meine, abgesehen von seinem verbindlichen Namen (der Standard Template Library) ...

C ++ wollte ursprünglich OOP-Konzepte in C präsentieren. Das heißt: Sie können anhand ihrer Klasse und Klassenhierarchie feststellen, was eine bestimmte Entität tun kann und was nicht (unabhängig davon, wie sie es tut). Einige Kompositionen von Fähigkeiten sind auf diese Weise aufgrund der Problematik der Mehrfachvererbung und der Tatsache, dass C ++ das Konzept von Schnittstellen auf etwas ungeschickte Weise unterstützt (im Vergleich zu Java usw.), schwieriger zu beschreiben, aber es ist da (und könnte es sein) verbessert).

Und dann kamen Vorlagen zusammen mit der STL ins Spiel. Die STL schien die klassischen OOP-Konzepte zu übernehmen und sie stattdessen mit Vorlagen in den Abfluss zu spülen.

Es sollte unterschieden werden zwischen Fällen, in denen Vorlagen zur Verallgemeinerung von Typen verwendet werden, bei denen die Typen selbst für den Betrieb der Vorlage irrelevant sind (z. B. Container). Ein vector<int>macht Sinn.

In vielen anderen Fällen (Iteratoren und Algorithmen) sollten Template-Typen jedoch einem "Konzept" (Eingabe-Iterator, Vorwärts-Iterator usw.) folgen, bei dem die tatsächlichen Details des Konzepts vollständig durch die Implementierung der Vorlage definiert werden Funktion / Klasse und nicht von der Klasse des Typs, der mit der Vorlage verwendet wird, was eine etwas gegen die Verwendung von OOP ist.

Zum Beispiel können Sie die Funktion mitteilen:

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

Update: Da es in der ursprünglichen Frage unklar war, kann ForwardIterator selbst als Vorlage verwendet werden, um jeden ForwardIterator-Typ zuzulassen. Das Gegenteil ist ForwardIterator als Konzept.

erwartet einen Forward Iterator nur anhand seiner Definition, wobei Sie entweder die Implementierung oder die Dokumentation überprüfen müssen, um:

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

Zwei Behauptungen, die ich für die Verwendung von Vorlagen aufstellen kann: Kompilierter Code kann effizienter gestaltet werden, indem die Vorlage für jeden verwendeten Typ maßgeschneidert kompiliert wird, anstatt vtables zu verwenden. Und die Tatsache, dass Vorlagen mit nativen Typen verwendet werden können.

Ich suche jedoch nach einem tieferen Grund, warum ich die klassische OOP zugunsten der Vorlage für die STL aufgeben sollte. (Angenommen, Sie lesen so weit: P)

OB OB
quelle
4
Sie können dies unter stackoverflow.com/questions/31693/… überprüfen . Die akzeptierte Antwort ist eine hervorragende Erklärung dafür, was Vorlagen Ihnen gegenüber Generika bieten.
James McMahon
6
@ Jonas: Das macht keinen Sinn. Die Einschränkung des Cache kostet Taktzyklen, weshalb dies wichtig ist. Am Ende des Tages sind es Taktzyklen, nicht der Cache, die die Leistung definieren. Speicher und Cache sind nur insoweit wichtig, als sie die verbrachten Taktzyklen beeinflussen. Darüber hinaus kann das Experiment leicht durchgeführt werden. Vergleichen Sie beispielsweise std :: for_Each, das mit einem functor-Argument aufgerufen wurde, mit dem entsprechenden OOP / vtable-Ansatz. Der Leistungsunterschied ist erstaunlich . Aus diesem Grund wird die Vorlagenversion verwendet.
Jalf
7
und es gibt keinen Grund, warum der redundante Code den icache füllen würde. Wenn ich in meinem Programm Vektor <char> und Vektor <int> instanziiere, warum sollte der Vektor <char> -Code in icache geladen werden, während ich den Vektor <int> verarbeite? Tatsächlich wird der Code für den Vektor <int> gekürzt, da er keinen Code für Casting, vtables und Indirektion enthalten muss.
Jalf
3
Alex Stepanov erklärt, warum Vererbung und Gleichheit nicht gut zusammenspielen.
Fredoverflow
6
@BerndJendrissek: Ähm, nah, aber nein du selbst. Ja, mehr Code kostet in Bezug auf Speicherbandbreite und Cache-Nutzung, falls er jemals tatsächlich verwendet wird . Es gibt jedoch keinen besonderen Grund, a zu erwarten vector<int>und vector<char>gleichzeitig verwendet zu werden. Sie könnten sicher sein, aber Sie könnten zwei beliebige Codeteile gleichzeitig verwenden. Das hat nichts mit Vorlagen, C ++ oder der STL zu tun. Es gibt nichts in der Instanziierung vector<int>die erfordert vector<char>Code geladen oder ausgeführt werden.
Jalf

Antworten:

607

Die kurze Antwort lautet "weil C ++ weitergezogen ist". Ja, in den späten 70ern wollte Stroustrup ein aktualisiertes C mit OOP-Funktionen erstellen, aber das ist schon lange her. Als die Sprache 1998 standardisiert wurde, war sie keine OOP-Sprache mehr. Es war eine Multi-Paradigmen-Sprache. Es hatte sicherlich eine gewisse Unterstützung für OOP-Code, aber es hatte auch eine vollständig überlagerte Vorlagensprache, es ermöglichte Metaprogrammierung zur Kompilierungszeit und die Leute hatten generische Programmierung entdeckt. Plötzlich schien OOP einfach nicht mehr so ​​wichtig zu sein. Nicht, wenn wir einfacheren, präziseren und effizienteren Code schreiben können, indem wir Techniken verwenden, die über Vorlagen und generische Programmierung verfügbar sind.

OOP ist nicht der heilige Gral. Es ist eine nette Idee, und es war eine ziemliche Verbesserung gegenüber prozeduralen Sprachen in den 70er Jahren, als es erfunden wurde. Aber es ist ehrlich gesagt nicht alles, worauf es ankommt. In vielen Fällen ist es ungeschickt und ausführlich und fördert nicht wirklich wiederverwendbaren Code oder Modularität.

Aus diesem Grund ist die C ++ - Community heute viel mehr an generischer Programmierung interessiert, und alle beginnen endlich zu erkennen, dass funktionale Programmierung auch ziemlich clever ist. OOP alleine ist einfach kein schöner Anblick.

Versuchen Sie, ein Abhängigkeitsdiagramm einer hypothetischen "OOP-ified" STL zu zeichnen. Wie viele Klassen müssten voneinander wissen? Es würde viele Abhängigkeiten geben. Könnten Sie nur den vectorHeader einfügen, ohne ihn auch zu bekommen iteratoroder gar iostreameinzuziehen? Die STL macht dies einfach. Ein Vektor kennt den von ihm definierten Iteratortyp und das ist alles. Die STL-Algorithmen wissen nichts . Sie müssen nicht einmal einen Iterator-Header enthalten, obwohl sie alle Iteratoren als Parameter akzeptieren. Was ist dann modularer?

Die STL folgt möglicherweise nicht den Regeln von OOP, wie Java sie definiert, aber erreicht sie nicht die Ziele von OOP? Erreicht es nicht Wiederverwendbarkeit, geringe Kopplung, Modularität und Kapselung?

Und erreicht es diese Ziele nicht besser als eine OOP-Version?

Warum die STL in die Sprache übernommen wurde, geschahen mehrere Dinge, die zur STL führten.

Zunächst wurden Vorlagen zu C ++ hinzugefügt. Sie wurden aus dem gleichen Grund hinzugefügt wie Generika zu .NET. Es schien eine gute Idee zu sein, Dinge wie "Container vom Typ T" schreiben zu können, ohne die Sicherheit des Typs wegzuwerfen. Natürlich war die Implementierung, für die sie sich entschieden hatten, viel komplexer und leistungsfähiger.

Dann stellten die Leute fest, dass der von ihnen hinzugefügte Vorlagenmechanismus noch leistungsfähiger war als erwartet. Und jemand fing an, mit Vorlagen zu experimentieren, um eine allgemeinere Bibliothek zu schreiben. Eine, die von der funktionalen Programmierung inspiriert ist und alle neuen Funktionen von C ++ nutzt.

Er präsentierte es dem C ++ - Sprachkomitee, das eine Weile brauchte, um sich daran zu gewöhnen, weil es so seltsam und anders aussah, aber letztendlich erkannte, dass es besser funktionierte als die traditionellen OOP-Äquivalente, die sie sonst einbeziehen müssten . Also nahmen sie einige Anpassungen vor und übernahmen es in die Standardbibliothek.

Es war keine ideologische Entscheidung, es war keine politische Entscheidung, "wollen wir OOP sein oder nicht", sondern eine sehr pragmatische. Sie bewerteten die Bibliothek und stellten fest, dass sie sehr gut funktionierte.

In jedem Fall sind beide Gründe, die Sie für die Bevorzugung der STL nennen, unbedingt erforderlich.

Die C ++ - Standardbibliothek muss effizient sein. Wenn es weniger effizient ist als beispielsweise der entsprechende handgerollte C-Code, wird es nicht verwendet. Das würde die Produktivität senken, die Wahrscheinlichkeit von Fehlern erhöhen und insgesamt nur eine schlechte Idee sein.

Und die STL muss mit primitiven Typen arbeiten, denn primitive Typen sind alles, was Sie in C haben, und sie sind ein Hauptbestandteil beider Sprachen. Wenn die STL nicht mit nativen Arrays funktionieren würde, wäre sie nutzlos .

Ihre Frage geht stark davon aus, dass OOP "am besten" ist. Ich bin gespannt warum. Sie fragen, warum sie "die klassische OOP aufgegeben" haben. Ich frage mich, warum sie dabei bleiben sollten. Welche Vorteile hätte es gehabt?

jalf
quelle
22
Es ist eine gute Zusammenfassung, aber ich möchte ein Detail hervorheben. Die STL ist kein "Produkt" von C ++. Tatsächlich existierte STL als Konzept vor C ++, und C ++ war zufällig eine effiziente Sprache mit (fast) genug Leistung für die generische Programmierung, sodass STL in C ++ geschrieben wurde.
Igor Krivokon
17
Ja, ich bin mir bewusst, dass der STL-Name nicht eindeutig ist. Ich kann mir aber keinen besseren Namen für "den Teil der C ++ - Standardbibliothek vorstellen, der der STL nachempfunden ist". Der De-facto-Name für diesen Teil der Standardbibliothek lautet nur "STL", obwohl er streng ungenau ist. :) Solange die Leute nicht STL als Namen für die gesamte Standardbibliothek verwenden (einschließlich IOStreams und der C stdlib-Header), bin ich glücklich. :)
Jalf
5
@einpoklum Und was genau würden Sie von einer abstrakten Basisklasse profitieren? Nehmen Sie std::setals Beispiel. Es erbt nicht von einer abstrakten Basisklasse. Wie schränkt das Ihre Nutzung von ein std::set? Gibt es etwas, das Sie mit a nicht tun können, std::setweil es nicht von einer abstrakten Basisklasse erbt?
Fredoverflow
22
@einpoklum Bitte werfen Sie einen Blick auf die Smalltalk-Sprache, die Alan Kay als OOP-Sprache entworfen hat, als er den Begriff OOP erfand. Es hatte keine Schnittstellen. Bei OOP geht es nicht um Schnittstellen oder abstrakte Basisklassen. Wollen Sie sagen, dass "Java, das nichts mit dem zu tun hat, was der Erfinder des Begriffs OOP im Sinn hatte, mehr OOP ist als C ++, was auch nichts mit dem zu tun hat, was der Erfinder des Begriffs OOP vorhatte"? Was Sie damit sagen wollen ist "C ++ ist nicht Java-ähnlich genug für meinen Geschmack". Das ist fair, hat aber nichts mit OOP zu tun.
Jalf
8
@ MasonWheeler, wenn diese Antwort ein Haufen Unsinn wäre, würden Sie nicht buchstäblich Hunderte von Entwicklern auf der ganzen Welt sehen, die +1 darüber abstimmen, wenn nur drei Leute etwas anderes tun
panda-34
88

Die direkteste Antwort auf das, worüber Sie meiner Meinung nach fragen / sich beschweren, lautet: Die Annahme, dass C ++ eine OOP-Sprache ist, ist eine falsche Annahme.

C ++ ist eine Multi-Paradigmen-Sprache. Es kann nach OOP-Prinzipien programmiert werden, es kann prozedural programmiert werden, es kann generisch programmiert werden (Vorlagen) und mit C ++ 11 (früher bekannt als C ++ 0x) können einige Dinge sogar funktional programmiert werden.

Die Entwickler von C ++ sehen dies als Vorteil an, und sie würden argumentieren, dass die Einschränkung von C ++, sich wie eine reine OOP-Sprache zu verhalten, wenn die generische Programmierung das Problem besser löst und allgemeiner ein Rückschritt wäre.

Tyler McHenry
quelle
4
"und mit C ++ 0x können einige Dinge sogar funktional programmiert werden" - es kann funktional ohne diese Funktionen programmiert werden, nur ausführlicher.
Jonas Kölker
3
@Tyler Wenn Sie C ++ auf reines OOP beschränken, bleibt Objective-C übrig.
Justicle
@ TylerMcHenry: Nachdem ich das gerade gefragt habe , habe ich gerade die gleiche Antwort wie Sie ausgesprochen! Nur ein Punkt. Ich wünschte, Sie würden die Tatsache hinzufügen, dass die Standardbibliothek nicht zum Schreiben von objektorientiertem Code verwendet werden kann.
Einpoklum
74

Meines Wissens nach bevorzugte Stroustrup ursprünglich ein Container-Design im "OOP-Stil" und sah keinen anderen Weg, dies zu tun. Alexander Stepanov ist derjenige, der für die STL verantwortlich ist, und seine Ziele beinhalteten nicht "objektorientiert machen" :

Das ist der grundlegende Punkt: Algorithmen werden auf algebraischen Strukturen definiert. Es dauerte noch ein paar Jahre, bis mir klar wurde, dass man den Begriff der Struktur erweitern muss, indem man regulären Axiomen Komplexitätsanforderungen hinzufügt. ... Ich glaube, dass Iteratortheorien für die Informatik ebenso zentral sind wie Ringtheorien oder Banachräume für die Mathematik. Jedes Mal, wenn ich einen Algorithmus betrachtete, versuchte ich, eine Struktur zu finden, auf der er definiert ist. Ich wollte also Algorithmen generisch beschreiben. Das mache ich gerne. Ich kann einen Monat lang an einem bekannten Algorithmus arbeiten, um dessen generische Darstellung zu finden. ...

Zumindest für mich ist STL die einzige Möglichkeit, wie Programmierung möglich ist. Es unterscheidet sich in der Tat stark von der C ++ - Programmierung, wie sie vorgestellt wurde und in den meisten Lehrbüchern immer noch vorgestellt wird. Aber Sie sehen, ich habe nicht versucht, in C ++ zu programmieren, ich habe versucht, den richtigen Weg zu finden, um mit Software umzugehen. ...

Ich hatte viele Fehlstarts. Zum Beispiel habe ich jahrelang versucht, eine Verwendung für Vererbung und Virtuals zu finden, bevor ich verstanden habe, warum dieser Mechanismus grundlegend fehlerhaft war und nicht verwendet werden sollte. Ich bin sehr froh, dass niemand alle Zwischenschritte sehen konnte - die meisten waren sehr dumm.

(Er erklärt, warum Vererbung und Virtuals - auch bekannt als objektorientiertes Design - im Rest des Interviews "grundlegend fehlerhaft waren und nicht verwendet werden sollten").

Nachdem Stepanov Stroustrup seine Bibliothek vorgestellt hatte, unternahmen Stroustrup und andere Herkulesbemühungen, um sie in den ISO C ++ - Standard aufzunehmen (gleiches Interview):

Die Unterstützung von Bjarne Stroustrup war entscheidend. Bjarne wollte wirklich STL im Standard und wenn Bjarne etwas will, bekommt er es. ... Er hat mich sogar gezwungen, Änderungen an der STL vorzunehmen, die ich niemals für andere vornehmen würde ... er ist die zielstrebigste Person, die ich kenne. Er erledigt Dinge. Er brauchte eine Weile, um zu verstehen, worum es bei STL ging, aber als er es tat, war er bereit, es durchzusetzen. Er trug auch zu STL bei, indem er sich für die Ansicht einsetzte, dass mehr als eine Art der Programmierung gültig sei - gegen mehr als ein Jahrzehnt gegen Flak und Hype - und eine Kombination aus Flexibilität, Effizienz, Überlastung und Typensicherheit anstrebte Vorlagen, die STL möglich machten. Ich möchte ganz klar sagen, dass Bjarne der herausragende Sprachdesigner meiner Generation ist.

Max Lybbert
quelle
2
Interessantes Interview. Ich bin mir ziemlich sicher, dass ich es vor einiger Zeit gelesen habe, aber es hat sich definitiv gelohnt, es noch einmal durchzugehen. :)
Jalf
3
Eines der interessantesten Interviews über Programmierung, die ich je gelesen habe. Obwohl es mich nach mehr Details
dürsten lässt
Viele der Beschwerden, die er über Sprachen wie Java vorbringt ("Sie können in Java kein generisches max () schreiben, das zwei Argumente eines Typs akzeptiert und einen Rückgabewert desselben Typs hat"), waren nur für sehr frühe Versionen relevant der Sprache, bevor Generika hinzugefügt wurden. Schon von Anfang an war bekannt, dass Generika hinzugefügt werden würden (sobald eine brauchbare Syntax / Semantik herausgefunden wurde), so dass seine Kritik weitgehend unbegründet ist. Ja, Generika in irgendeiner Form werden benötigt, um die Typensicherheit in einer statisch typisierten Sprache zu gewährleisten, aber nein, das macht OO nicht wertlos.
Einige Guy
1
@SomeGuy Sie sind keine Beschwerden über Java an sich. Er spricht von " der" Standard "OO-Programmierung von SmallTalk oder beispielsweise Java ". Das Interview stammt aus den späten 90ern (er erwähnt die Arbeit bei SGI, die er im Jahr 2000 verließ, um bei AT & T zu arbeiten). Generika wurden erst 2004 in Version 1.5 zu Java hinzugefügt und sind eine Abweichung vom "Standard" -OO-Modell.
Melpomene
24

Die Antwort findet sich in diesem Interview mit Stepanov, dem Autor der STL:

Ja. STL ist nicht objektorientiert. Ich denke, dass Objektorientierung fast genauso ein Scherz ist wie künstliche Intelligenz. Ich habe noch keinen interessanten Code gesehen, der von diesen OO-Leuten stammt.

StackedCrooked
quelle
Schönes Juwel; Weißt du aus welchem ​​Jahr es ist?
Kos
2
@Kos, laut web.archive.org/web/20000607205939/http://www.stlport.org/… ist die erste Version der verlinkten Seite vom 7. Juni 2001. Auf der Seite unten steht Copyright 2001- 2008.
AlfC
@Kos Stepanov erwähnt in der ersten Antwort die Arbeit bei SGI. Er hat SGI im Mai 2000 verlassen, daher ist das Interview vermutlich älter.
Melpomene
18

Warum wäre ein reines OOP-Design für eine Datenstruktur- und Algorithmusbibliothek besser ?! OOP ist nicht die Lösung für alles.

IMHO, STL ist die eleganteste Bibliothek, die ich je gesehen habe :)

für Ihre Frage,

Sie benötigen keinen Laufzeitpolymorphismus. Für STL ist es ein Vorteil, die Bibliothek mithilfe statischen Polymorphismus zu implementieren. Dies bedeutet Effizienz. Versuchen Sie, eine generische Sortierung oder Entfernung oder einen beliebigen Algorithmus zu schreiben, der für ALLE Container gilt! Ihre Sortierung in Java würde Funktionen aufrufen, die durch n-Ebenen dynamisch sind, um ausgeführt zu werden!

Sie brauchen dumme Dinge wie Boxen und Unboxen, um böse Annahmen der sogenannten Pure OOP-Sprachen zu verbergen.

Das einzige Problem, das ich mit STL und Vorlagen im Allgemeinen sehe, sind die schrecklichen Fehlermeldungen. Was mit Concepts in C ++ 0X gelöst wird.

Das Vergleichen von STL mit Sammlungen in Java ist wie das Vergleichen von Taj Mahal mit meinem Haus :)

AraK
quelle
12
Was, Taj Mahal ist klein und elegant, und Ihr Haus hat die Größe eines Berges und ein komplettes Durcheinander? ;)
Jalf
Konzepte sind nicht mehr Teil von c ++ 0x. Einige der Fehlermeldungen können mit static_assertvielleicht vorbelegt werden .
KitsuneYMG
GCC 4.6 hat Vorlagenfehlermeldungen verbessert, und ich glaube, dass 4.7+ damit noch besser sind.
David Stone
Ein Konzept ist im Wesentlichen die "Schnittstelle", nach der das OP gefragt hat. Der einzige Unterschied besteht darin, dass die "Vererbung" von einem Konzept implizit ist (wenn eine Klasse über alle richtigen Elementfunktionen verfügt, ist sie automatisch ein Subtyp des Konzepts) und nicht explizit (eine Java-Klasse muss explizit deklarieren, dass sie eine Schnittstelle implementiert). . Sowohl implizite als auch explizite Untertypen sind jedoch gültige OO, und einige OO-Sprachen haben implizite Vererbung, die genau wie Konzepte funktioniert. Was hier also gesagt wird, ist im Grunde "OO ist scheiße: Verwenden Sie Vorlagen. Aber Vorlagen haben Probleme, also verwenden Sie Konzepte (die OO sind)."
Einige Guy
11

Templated-Typen sollen einem "Konzept" (Input Iterator, Forward Iterator usw.) folgen, bei dem die tatsächlichen Details des Konzepts vollständig durch die Implementierung der Vorlagenfunktion / -klasse und nicht durch die Klasse des Typs definiert werden wird mit der Vorlage verwendet, die eine etwas gegen die Verwendung von OOP ist.

Ich denke, Sie verstehen die beabsichtigte Verwendung von Konzepten durch Vorlagen falsch. Forward Iterator zum Beispiel ist ein sehr genau definiertes Konzept. Informationen zu den Ausdrücken, die gültig sein müssen, damit eine Klasse ein Vorwärtsiterator ist, und zu ihrer Semantik einschließlich der Komplexität der Berechnungen finden Sie im Standard oder unter http://www.sgi.com/tech/stl/ForwardIterator.html (Sie müssen den Links zu Input, Output und Trivial Iterator folgen, um alles zu sehen).

Dieses Dokument ist eine perfekte Schnittstelle, und "die tatsächlichen Details des Konzepts" werden genau dort definiert. Sie werden nicht durch die Implementierungen von Vorwärtsiteratoren definiert, und sie werden auch nicht durch die Algorithmen definiert, die Vorwärtsiteratoren verwenden.

Es gibt drei Unterschiede im Umgang mit Schnittstellen zwischen STL und Java:

1) STL definiert gültige Ausdrücke unter Verwendung des Objekts, während Java Methoden definiert, die für das Objekt aufrufbar sein müssen. Natürlich kann ein gültiger Ausdruck ein Methodenaufruf (Elementfunktion) sein, muss es aber nicht sein.

2) Java-Schnittstellen sind Laufzeitobjekte, während STL-Konzepte zur Laufzeit selbst mit RTTI nicht sichtbar sind.

3) Wenn Sie die erforderlichen gültigen Ausdrücke für ein STL-Konzept nicht gültig machen, wird ein nicht angegebener Kompilierungsfehler angezeigt, wenn Sie eine Vorlage mit dem Typ instanziieren. Wenn Sie eine erforderliche Methode einer Java-Schnittstelle nicht implementieren können, wird ein bestimmter Kompilierungsfehler angezeigt.

Dieser dritte Teil ist, wenn Sie eine Art (zur Kompilierungszeit) "Ententypisierung" mögen: Schnittstellen können implizit sein. In Java sind Schnittstellen etwas explizit: Eine Klasse "ist" Iterable genau dann, wenn sie besagt, dass sie Iterable implementiert. Der Compiler kann überprüfen, ob die Signaturen seiner Methoden alle vorhanden und korrekt sind, aber die Semantik ist immer noch implizit (dh sie sind entweder dokumentiert oder nicht, aber nur mehr Code (Komponententests) kann Ihnen sagen, ob die Implementierung korrekt ist).

In C ++ sind wie in Python sowohl Semantik als auch Syntax implizit, obwohl Sie in C ++ (und in Python, wenn Sie den Präprozessor mit starker Typisierung erhalten) Hilfe vom Compiler erhalten. Wenn ein Programmierer eine Java-ähnliche explizite Deklaration von Schnittstellen durch die implementierende Klasse benötigt, besteht der Standardansatz darin, Typmerkmale zu verwenden (und Mehrfachvererbung kann verhindern, dass dies zu ausführlich ist). Was im Vergleich zu Java fehlt, ist eine einzelne Vorlage, die ich mit meinem Typ instanziieren kann und die nur dann kompiliert wird, wenn alle erforderlichen Ausdrücke für meinen Typ gültig sind. Dies würde mir sagen, ob ich alle erforderlichen Bits implementiert habe, "bevor ich es benutze". Das ist eine Annehmlichkeit, aber es ist nicht der Kern von OOP (und es testet immer noch nicht die Semantik,

STL kann für Ihren Geschmack ausreichend OO sein oder auch nicht, aber es trennt die Schnittstelle sicher sauber von der Implementierung. Es fehlt Javas Fähigkeit, über Schnittstellen nachzudenken, und es werden Verstöße gegen die Schnittstellenanforderungen unterschiedlich gemeldet.

Sie können der Funktion sagen, dass ... einen Forward Iterator nur anhand seiner Definition erwartet, in der Sie entweder die Implementierung oder die Dokumentation für ...

Persönlich denke ich, dass implizite Typen eine Stärke sind, wenn sie angemessen verwendet werden. Der Algorithmus sagt, was er mit seinen Vorlagenparametern macht, und der Implementierer stellt sicher, dass diese Dinge funktionieren: Es ist genau der gemeinsame Nenner dessen, was "Schnittstellen" tun sollen. Darüber hinaus ist es unwahrscheinlich, dass Sie mit STL beispielsweise std::copydie Forward-Deklaration in einer Header-Datei finden. Programmierer sollten anhand ihrer Dokumentation und nicht nur anhand der Funktionssignatur herausfinden, was eine Funktion benötigt. Dies gilt für C ++, Python oder Java. Es gibt Einschränkungen, was mit dem Tippen in einer beliebigen Sprache erreicht werden kann, und der Versuch, mit dem Tippen etwas zu tun, was nicht funktioniert (Semantik überprüfen), wäre ein Fehler.

Allerdings benennen STL-Algorithmen ihre Vorlagenparameter normalerweise so, dass klar wird, welches Konzept erforderlich ist. Dies dient jedoch dazu, nützliche zusätzliche Informationen in der ersten Zeile der Dokumentation bereitzustellen und Vorwärtserklärungen nicht informativer zu gestalten. Es gibt mehr Dinge, die Sie wissen müssen, als in den Parametertypen gekapselt werden können. Sie müssen also die Dokumente lesen. (Zum Beispiel in Algorithmen, die einen Eingabebereich und einen Ausgabe-Iterator verwenden, benötigt der Ausgabe-Iterator wahrscheinlich genug "Platz" für eine bestimmte Anzahl von Ausgaben, basierend auf der Größe des Eingabebereichs und möglicherweise den darin enthaltenen Werten. Versuchen Sie, diesen stark einzugeben. )

Hier ist Bjarne zu explizit deklarierten Schnittstellen: http://www.artima.com/cppsource/cpp0xP.html

In Generika muss ein Argument von einer Klasse sein, die von einer Schnittstelle abgeleitet ist (das C ++ - Äquivalent zur Schnittstelle ist eine abstrakte Klasse), die in der Definition des Generikums angegeben ist. Das bedeutet, dass alle generischen Argumenttypen in eine Hierarchie passen müssen. Dies führt zu unnötigen Einschränkungen bei den Designs und erfordert eine unangemessene Voraussicht der Entwickler. Wenn Sie beispielsweise ein Generikum schreiben und ich eine Klasse definiere, können Benutzer meine Klasse nicht als Argument für Ihr Generikum verwenden, es sei denn, ich kenne die von Ihnen angegebene Schnittstelle und habe meine Klasse daraus abgeleitet. Das ist starr.

Wenn Sie es anders herum betrachten, können Sie mit duck tippen eine Schnittstelle implementieren, ohne zu wissen, dass die Schnittstelle vorhanden ist. Oder jemand kann eine Schnittstelle absichtlich so schreiben, dass Ihre Klasse sie implementiert, nachdem er Ihre Dokumente konsultiert hat, um festzustellen, dass er nicht nach etwas fragt, was Sie noch nicht getan haben. Das ist flexibel.

Steve Jessop
quelle
Auf explizit deklarierten Schnittstellen zwei Wörter: Typklassen. (Was Stepanov bereits unter "Konzept" versteht.)
pyon
"Wenn Sie die erforderlichen gültigen Ausdrücke für ein STL-Konzept nicht gültig machen, wird ein nicht angegebener Kompilierungsfehler angezeigt, wenn Sie eine Vorlage mit dem Typ instanziieren." -- das ist falsch. Die Übergabe von etwas an die stdBibliothek, das nicht mit einem Konzept übereinstimmt, ist normalerweise "schlecht geformt, keine Diagnose erforderlich".
Yakk - Adam Nevraumont
Es stimmt, ich habe schnell und locker mit dem Begriff "gültig" gespielt. Ich meinte nur, wenn der Compiler einen der erforderlichen Ausdrücke nicht kompilieren kann, wird er etwas melden.
Steve Jessop
8

"OOP bedeutet für mich nur Messaging, lokale Aufbewahrung und Schutz sowie das Verbergen von Staatsprozessen und die extreme Spätbindung aller Dinge. Dies kann in Smalltalk und in LISP erfolgen. Es gibt möglicherweise andere Systeme, in denen dies möglich ist, aber Ich bin mir ihrer nicht bewusst. " - Alan Kay, Schöpfer von Smalltalk.

C ++, Java und die meisten anderen Sprachen sind alle ziemlich weit vom klassischen OOP entfernt. Das Argumentieren für Ideologien ist jedoch nicht besonders produktiv. C ++ ist in keiner Weise rein, daher implementiert es Funktionen, die zu diesem Zeitpunkt pragmatisch sinnvoll erscheinen.

Ben Hughes
quelle
7

STL begann mit der Absicht, eine große Bibliothek bereitzustellen, die die am häufigsten verwendeten Algorithmen abdeckt - mit dem Ziel eines konsistenten Verhaltens und einer konsistenten Leistung . Die Vorlage war ein Schlüsselfaktor, um diese Implementierung und dieses Ziel zu verwirklichen.

Nur um eine weitere Referenz zu liefern:

Al Stevens interviewt Alex Stepanov im März 1995 von DDJ:

Stepanov erklärte seine Arbeitserfahrung und seine Entscheidung für eine große Bibliothek von Algorithmen, die sich schließlich zu STL entwickelten.

Erzählen Sie uns etwas über Ihr langfristiges Interesse an generischer Programmierung

..... Dann wurde mir eine Stelle bei Bell Laboratories angeboten, die in der C ++ - Gruppe für C ++ - Bibliotheken arbeiten. Sie fragten mich, ob ich es in C ++ machen könnte. Natürlich kannte ich C ++ nicht und natürlich sagte ich, ich könnte. Aber ich konnte es in C ++ nicht tun, weil C ++ 1987 keine Vorlagen hatte, die für die Aktivierung dieses Programmierstils wesentlich sind. Vererbung war der einzige Mechanismus, um Generizität zu erhalten, und es war nicht ausreichend.

Selbst jetzt ist die C ++ - Vererbung für die generische Programmierung nicht sehr nützlich. Lassen Sie uns diskutieren, warum. Viele Menschen haben versucht, mithilfe der Vererbung Datenstrukturen und Containerklassen zu implementieren. Wie wir jetzt wissen, gab es nur wenige, wenn überhaupt erfolgreiche Versuche. Die C ++ - Vererbung und der damit verbundene Programmierstil sind dramatisch eingeschränkt. Es ist unmöglich, ein Design zu implementieren, das so trivial ist wie Gleichheit. Wenn Sie mit einer Basisklasse X im Stammverzeichnis Ihrer Hierarchie beginnen und für diese Klasse einen virtuellen Gleichheitsoperator definieren, der ein Argument vom Typ X verwendet, leiten Sie die Klasse Y aus der Klasse X ab. Was ist die Schnittstelle der Gleichheit? Es hat eine Gleichheit, die Y mit X vergleicht. Am Beispiel von Tieren (OO Menschen lieben Tiere) definieren Sie Säugetiere und leiten Giraffen von Säugetieren ab. Definieren Sie dann einen Elementfunktionspartner. wo sich das Tier mit dem Tier paart und ein Tier zurückgibt. Dann leitet man eine Giraffe vom Tier ab und natürlich hat sie einen Funktionspartner, bei dem sich die Giraffe mit dem Tier paart und ein Tier zurückgibt. Es ist definitiv nicht das, was du willst. Während die Paarung für C ++ - Programmierer möglicherweise nicht sehr wichtig ist, ist sie gleich. Ich kenne keinen einzigen Algorithmus, bei dem keine Gleichheit verwendet wird.

Yowkee
quelle
5

Das Grundproblem mit

void MyFunc(ForwardIterator *I);

Wie erhalten Sie sicher den Typ der Sache, die der Iterator zurückgibt? Bei Vorlagen wird dies zur Kompilierungszeit für Sie erledigt.


quelle
1
Nun, ich auch: 1. Versuchen Sie nicht, es zu bekommen, da ich generischen Code schreibe. Oder 2. Holen Sie es sich mit dem Reflexionsmechanismus, den C ++ heutzutage bietet.
Einpoklum
2

Stellen wir uns die Standardbibliothek für einen Moment als eine Datenbank mit Sammlungen und Algorithmen vor.

Wenn Sie die Geschichte von Datenbanken studiert haben, wissen Sie zweifellos, dass Datenbanken zu Beginn meist "hierarchisch" waren. Hierarchische Datenbanken entsprachen sehr genau der klassischen OOP - insbesondere der von Smalltalk verwendeten Sorte mit einfacher Vererbung.

Im Laufe der Zeit stellte sich heraus, dass hierarchische Datenbanken verwendet werden konnten, um fast alles zu modellieren, aber in einigen Fällen war das Einzelvererbungsmodell ziemlich einschränkend. Wenn Sie eine Holztür hatten, war es praktisch, sie entweder als Tür oder als Stück Rohmaterial (Stahl, Holz usw.) betrachten zu können.

Also erfanden sie Netzwerkmodelldatenbanken. Netzwerkmodelldatenbanken entsprechen sehr genau der Mehrfachvererbung. C ++ unterstützt die Mehrfachvererbung vollständig, während Java eine begrenzte Form unterstützt (Sie können nur von einer Klasse erben, aber auch so viele Schnittstellen implementieren, wie Sie möchten).

Sowohl hierarchische Modell- als auch Netzwerkmodelldatenbanken sind größtenteils aus dem allgemeinen Gebrauch verschwunden (obwohl einige in ziemlich spezifischen Nischen verbleiben). Für die meisten Zwecke wurden sie durch relationale Datenbanken ersetzt.

Der Hauptgrund für die Übernahme relationaler Datenbanken war die Vielseitigkeit. Das relationale Modell ist funktional eine Obermenge des Netzwerkmodells (das wiederum eine Obermenge des hierarchischen Modells ist).

C ++ ist weitgehend dem gleichen Weg gefolgt. Die Entsprechung zwischen Einzelvererbung und dem hierarchischen Modell sowie zwischen Mehrfachvererbung und Netzwerkmodell ist ziemlich offensichtlich. Die Entsprechung zwischen C ++ - Vorlagen und dem hierarchischen Modell mag weniger offensichtlich sein, passt aber trotzdem ziemlich gut zusammen.

Ich habe keinen formalen Beweis dafür gesehen, aber ich glaube, dass die Funktionen von Vorlagen eine Obermenge derjenigen sind, die durch Mehrfachvererbung bereitgestellt werden (was eindeutig eine Obermenge einer einzelnen Vererbung ist). Der einzige schwierige Teil ist, dass Vorlagen größtenteils statisch gebunden sind - das heißt, die gesamte Bindung erfolgt zur Kompilierungszeit und nicht zur Laufzeit. Daher kann ein formaler Beweis dafür, dass die Vererbung eine Obermenge der Vererbungsfähigkeiten bietet, durchaus schwierig und komplex sein (oder sogar unmöglich sein).

Auf jeden Fall denke ich, dass dies der Hauptgrund dafür ist, dass C ++ keine Vererbung für seine Container verwendet - es gibt keinen wirklichen Grund dafür, da die Vererbung nur einen Teil der von Vorlagen bereitgestellten Funktionen bietet. Da Vorlagen in einigen Fällen grundsätzlich erforderlich sind, können sie auch fast überall verwendet werden.

Jerry Sarg
quelle
0

Wie macht man Vergleiche mit ForwardIterator *? Das heißt, wie überprüfen Sie, ob der Artikel, den Sie haben, das ist, wonach Sie suchen, oder ob Sie ihn passiert haben?

Meistens würde ich so etwas verwenden:

void MyFunc(ForwardIterator<MyType>& i)

Das heißt, ich weiß, dass ich auf MyType zeige, und ich weiß, wie ich diese vergleichen kann. Obwohl es wie eine Vorlage aussieht, ist es nicht wirklich (kein Schlüsselwort "Vorlage").

Tanktalus
quelle
Sie können einfach die Operatoren <,> und = des Typs verwenden und nicht wissen, was diese sind (obwohl dies möglicherweise nicht das ist, was Sie gemeint haben)
lhahne
Je nach Kontext ergeben diese möglicherweise keinen Sinn oder funktionieren möglicherweise einwandfrei. Schwer zu sagen, ohne mehr über MyType zu wissen, was der Benutzer vermutlich tut und wir nicht.
Tanktalus
0

Diese Frage hat viele gute Antworten. Es sollte auch erwähnt werden, dass Vorlagen ein offenes Design unterstützen. Beim gegenwärtigen Stand objektorientierter Programmiersprachen muss bei solchen Problemen das Besuchermuster verwendet werden, und echte OOP sollte mehrere dynamische Bindungen unterstützen. Siehe Open Multi-Methods für C ++, P. Pirkelbauer et al. für sehr interessante Lektüre.

Ein weiterer interessanter Punkt von Vorlagen ist, dass sie auch für den Laufzeitpolymorphismus verwendet werden können. Beispielsweise

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;
    }

Beachten Sie, dass diese Funktion auch funktioniert, wenn Valuees sich um einen Vektor handelt ( nicht um std :: vector, der aufgerufen werden sollte std::dynamic_array, um Verwirrung zu vermeiden).

Wenn funces klein ist, wird diese Funktion durch Inlining viel gewinnen. Anwendungsbeispiel

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

In diesem Fall sollten Sie die genaue Antwort kennen (2.718 ...), aber es ist einfach, eine einfache ODE ohne elementare Lösung zu konstruieren (Hinweis: Verwenden Sie ein Polynom in y).

Jetzt haben Sie einen großen Ausdruck in funcund verwenden den ODE-Solver an vielen Stellen, sodass Ihre ausführbare Datei überall mit Vorlageninstanziierungen verschmutzt wird. Was ist zu tun? Als erstes fällt auf, dass ein regulärer Funktionszeiger funktioniert. Dann möchten Sie Currying hinzufügen, damit Sie eine Schnittstelle und eine explizite Instanziierung schreiben

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);

Die obige Instanziierung funktioniert jedoch nur für double, warum nicht die Schnittstelle als Vorlage schreiben:

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

und spezialisieren Sie sich auf einige gängige Werttypen:

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)

Wenn die Funktion zuerst um eine Schnittstelle herum entworfen worden wäre, wären Sie gezwungen gewesen, von diesem ABC zu erben. Jetzt haben Sie diese Option sowie Funktionszeiger, Lambda oder ein anderes Funktionsobjekt. Der Schlüssel hier ist, dass wir haben müssen operator()(), und wir müssen in der Lage sein, einige arithmetische Operatoren für den Rückgabetyp zu verwenden. Somit würde die Vorlagenmaschinerie in diesem Fall brechen, wenn C ++ keine Operatorüberladung hätte.

user877329
quelle
-1

Das Konzept, Schnittstelle von Schnittstelle zu trennen und die Implementierungen austauschen zu können, ist für die objektorientierte Programmierung nicht wesentlich. Ich glaube, es ist eine Idee, die in der komponentenbasierten Entwicklung wie Microsoft COM entwickelt wurde. (Siehe meine Antwort zu Was ist komponentengesteuerte Entwicklung?) Als ich aufwuchs und C ++ lernte, wurden die Leute von Vererbung und Polymorphismus hochgespielt. Erst in den 90er Jahren sagten die Leute "Programmieren zu einer 'Schnittstelle', nicht zu einer 'Implementierung'" und "Bevorzugen 'die Objektzusammensetzung' gegenüber 'Klassenvererbung'." (beide übrigens aus GoF zitiert).

Dann kam Java mit integriertem Garbage Collector und interfaceSchlüsselwort, und plötzlich wurde es praktisch, Schnittstelle und Implementierung tatsächlich zu trennen. Bevor Sie es wissen, wurde die Idee Teil der OO. C ++, Vorlagen und STL sind älter als all dies.

Eugene Yokota
quelle
Einverstanden, dass Schnittstellen nicht nur OO sind. Aber der Fähigkeitspolymorphismus im Typensystem ist (es war in Simula in den 60ern). Modulschnittstellen gab es in Modula-2 und Ada, aber diese funktionierten im Typsystem meiner Meinung nach anders.
Andygavin