Zu diesem Zeitpunkt ist diese Bibliothek experimentell und eine reine Neugier. Es ist keine Stabilität der Schnittstelle oder Qualität der Implementierung garantiert. Verwenden Sie Ihre eigenen Risiken.
Dyno löst das Problem des Laufzeitpolymorphismus besser als Vanille C ++. Es bietet eine Möglichkeit, Schnittstellen zu definieren, die nicht intrusiv erfüllt werden können, und bietet eine vollständig anpassbare Möglichkeit, polymorphe Objekte zu speichern und auf virtuelle Methoden zu versenden. Es erfordert keine Vererbung, Haufen Allokation oder Verlassen der komfortablen Welt der Wertsemantik und kann dies tun, während es Vanille C ++ übertrifft.
Dyno ist eine reine Bibliotheksimplementierung von sogenannten Rost-Merkmalobjekten, GO-Schnittstellen, Haskell-Typklassen und virtuellen Konzepten. Unter der Motorhaube verwendet es eine C ++ - Technik, die als Typ -Löschen bezeichnet wird. Dies ist die Idee hinter std::any , std::function und vielen anderen nützlichen Typen.
# include < dyno.hpp >
# include < iostream >
using namespace dyno ::literals ;
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { };
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](T const & self, std::ostream& out) { self. draw (out); }
);
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}Wenn Sie dies als zu viel Kesselplatte finden und mit einem Makro stehen können, ist das Folgende gleichwertig:
# include < dyno.hpp >
# include < iostream >
// Define the interface of something that can be drawn
DYNO_INTERFACE (Drawable,
(draw, void (std::ostream&) const )
);
struct Square {
void draw (std::ostream& out) const { out << " Square " ; }
};
struct Circle {
void draw (std::ostream& out) const { out << " Circle " ; }
};
void f (Drawable const & d) {
d. draw (std::cout);
}
int main () {
f (Square{}); // prints Square
f (Circle{}); // prints Circle
}Dies ist eine C ++ 17 -Bibliothek. Es werden keine Anstrengungen unternommen, um ältere Compiler zu unterstützen (Entschuldigung). Es ist bekannt, dass die Bibliothek mit den folgenden Compilern zusammenarbeitet:
| Compiler | Version |
|---|---|
| GCC | > = 7 |
| Klang | > = 4.0 |
| Apfelklang | > = 9.1 |
Die Bibliothek hängt von Boost.hana und Boost.Callabletraits ab. Die Unit -Tests hängen von libawful ab und die Benchmarks hängen von Google Benchmark, Boost.Typeerasure und mpark.variant ab, aber Sie müssen nicht die Bibliothek verwenden. Für die lokale Entwicklung kann das Skript von dependencies/install.sh verwendet werden, um alle Abhängigkeiten automatisch zu installieren.
Dyno ist eine Nur-Header-Bibliothek, daher gibt es nichts zu bauen. Fügen Sie einfach den Header include/ Verzeichnis hinzu (und stellen Sie sicher, dass die Abhängigkeiten zufrieden sind), und Sie können loslegen. Es gibt jedoch Unit -Tests, Beispiele und Benchmarks, die gebaut werden können:
(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH= " ${PWD} /../dependencies/install " ) # Setup the build directory
cmake --build build --target examples # Build and run the examples
cmake --build build --target tests # Build and run the unit tests
cmake --build build --target check # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks Bei der Programmierung ergibt sich die Notwendigkeit, Objekte mit einer gemeinsamen Schnittstelle zu manipulieren, jedoch mit einem anderen dynamischen Typ sehr häufig. C ++ löst dies mit Vererbung:
struct Drawable {
virtual void draw (std::ostream& out) const = 0;
};
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
struct Circle : Drawable {
virtual void draw (std::ostream& out) const override final { ... }
};
void f (Drawable const * drawable) {
drawable-> draw (std::cout);
}Dieser Ansatz hat jedoch mehrere Nachteile. Es ist
Aufdringlich
Damit Square und Circle die Drawable Schnittstelle erfüllen können, müssen beide von der Drawable Basisklasse erben. Dies erfordert die Lizenz, diese Klassen zu ändern, was die Vererbung sehr unbestimmt macht. Wie würden Sie beispielsweise eine std::vector<int> die Drawable Schnittstelle erfüllen? Sie können einfach nicht.
Mit Wertsemantik unvereinbar
Die Erbschaft erfordert, dass Sie polymorphe Zeiger oder Verweise auf Objekte anstelle der Objekte selbst übergeben, die sehr schlecht mit dem Rest der Sprache und der Standardbibliothek spielen. Wie würden Sie beispielsweise einen Vektor von Drawable S kopieren? Sie müssten eine virtuelle clone() -Methode bereitstellen, aber jetzt haben Sie nur Ihre Benutzeroberfläche durcheinander gebracht.
Eng mit dynamischem Speicher gekoppelt
Aufgrund der Mangel an Wertsemantik zuweisen wir normalerweise diese polymorphen Objekte auf dem Haufen. Dies ist sowohl schrecklich ineffizient als auch semantisch falsch, da wir die Chancen bestehen, dass wir die dynamische Speicherdauer überhaupt nicht benötigen, und ein Objekt mit automatischer Speicherdauer (z. B. am Stapel) wäre ausreichend gewesen.
Verhindert das Inline -Inline
In 95% der Fälle rufen wir am Ende eine virtuelle Methode über einen polymorphen Zeiger oder eine Referenz auf. Dies erfordert drei Indirektionen: einen zum Laden des Zeigers auf das VTABLE im Objekt, einen zum Laden des richtigen Eintrags in die VTABLE und einen für den indirekten Aufruf des Funktionszeigers. All dies macht es dem Compiler schwer, gute Einbindungsentscheidungen zu treffen. Es stellt sich jedoch heraus, dass alle diese Indirektionen mit Ausnahme des indirekten Aufrufs vermieden werden können.
Leider ist dies die Wahl, die C ++ für uns getroffen hat, und dies sind die Regeln, an die wir gebunden sind, wenn wir dynamischen Polymorphismus benötigen. Oder ist es wirklich?
Dyno löst das Problem des Laufzeitpolymorphismus in C ++ ohne die oben aufgeführten Nachteile und viele weitere Leckereien. Es ist:
Nicht intrusiv
Eine Schnittstelle kann von einem Typ erfüllt werden, ohne dass Änderungen an diesem Typ erforderlich sind. Heck, ein Typ kann sogar die gleiche Schnittstelle auf unterschiedliche Weise erfüllen! Mit Dyno können Sie lächerliche Klassenhierarchien auf Wiedersehen küssen.
100% basierend auf Wertsemantik
Polymorphe Objekte können mit ihrer natürlichen Wertsemantik übergeben werden. Sie müssen Ihre polymorphen Objekte kopieren? Sicher, stellen Sie einfach sicher, dass sie einen Kopierkonstruktor haben. Sie möchten sicherstellen, dass sie nicht kopiert werden? Sicher, markieren Sie es als gelöscht. Mit Dyno sind dumme clone() und die Verbreitung von Zeigern in APIs Dinge der Vergangenheit.
Nicht mit einer spezifischen Speicherstrategie verbunden
Die Art und Weise, wie ein polymorphes Objekt gespeichert wird, ist wirklich ein Implementierungsdetail und sollte die Art und Weise, wie Sie dieses Objekt verwenden, nicht beeinträchtigen. Dyno gibt Ihnen die vollständige Kontrolle über die Art und Weise, wie Ihre Objekte gespeichert werden. Sie haben viele kleine polymorphe Objekte? Klar, lass sie sie in einem lokalen Puffer aufbewahren und eine Zuteilung vermeiden. Oder macht es vielleicht Sinn für Sie, Dinge auf dem Haufen zu speichern? Sicher, mach weiter.
Flexibler Versandmechanismus, um die bestmögliche Leistung zu erzielen
Das Speichern eines Zeigers auf einen VTable ist nur eine von vielen verschiedenen Implementierungsstrategien für die Durchführung eines dynamischen Versandes. Dyno gibt Ihnen die vollständige Kontrolle darüber, wie der dynamische Versand auftritt, und kann in einigen Fällen Vtables tatsächlich schlagen. Wenn Sie eine Funktion haben, die in einer heißen Schleife aufgerufen wird, können Sie sie beispielsweise direkt im Objekt speichern und die VTABLE -Indirektion überspringen. Sie können auch anwendungsspezifisches Wissen verwenden, den der Compiler möglicherweise einige dynamische Anrufe optimieren muss-Devirtualisierung auf Bibliotheksebene.
Zunächst definieren Sie zunächst eine generische Schnittstelle und geben ihr einen Namen. Dyno bietet dazu eine einfache domänenspezifische Sprache. Lassen Sie uns beispielsweise eine Drawable des Schnittstellens definieren, die Typen beschreibt, die gezeichnet werden können:
# include < dyno.hpp >
using namespace dyno ::literals ;
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::method< void (std::ostream&) const >
)) { }; Dies definiert als Drawable als eine Schnittstelle für alles, das eine Methode namens draw mit einem Verweis auf ein std::ostream enthält. Dyno nennt diese Schnittstellen dynamische Konzepte , da sie Anforderungen, die von einem Typ erfüllt werden sollen (wie C ++ - Konzepte), beschreiben. Im Gegensatz zu C ++ - Konzepten werden diese dynamischen Konzepte jedoch verwendet, um Laufzeitschnittstellen zu generieren, wodurch die Namensdynamik . Die obige Definition entspricht im Wesentlichen wie folgt:
struct Drawable {
virtual void draw (std::ostream&) const = 0;
};Sobald die Schnittstelle definiert ist, besteht der nächste Schritt darin, tatsächlich einen Typ zu erstellen, der diese Schnittstelle erfüllt. Mit der Vererbung würden Sie so etwas schreiben:
struct Square : Drawable {
virtual void draw (std::ostream& out) const override final {
out << " square " << std::endl;
}
};Bei Dyno ist der Polymorphismus nicht intrusiv und wird stattdessen über eine sogenannte Konzeptkarte bereitgestellt (nach C ++ 0x-Konzeptkarten):
struct Square { /* ... */ };
template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
" draw " _s = [](Square const & square, std::ostream& out) {
out << " square " << std::endl;
}
);Dieses Konstrukt ist die Spezialisierung einer C ++ 14 -variablen Vorlage mit dem Namen
concept_map, die imdyno::Namespace definiert ist. Anschließend initialisieren wir diese Spezialisierung mitdyno::make_concept_map(...).
Der erste Parameter der Lambda ist der implizite *this Parameter, der impliziert ist, wenn wir draw als eine Methode oben deklarierten. Es ist auch möglich, Nichtmitgliedfunktionen zu löschen (siehe den entsprechenden Abschnitt).
Diese Konzeptkarte definiert, wie der Typ Square das Drawable Konzept erfüllt. In gewissem Sinne ordnet es das Square auf seine Implementierung des Konzepts ab, das die Bezeichnung motiviert. Wenn ein Typ die Anforderungen eines Konzepts erfüllt, sagen wir, dass die Typmodelle (oder ein Modell von) dieses Konzept sind. Jetzt, da das Square ein Modell des Drawable Konzepts ist, möchten wir ein Square polymorphes als Drawable verwenden. Mit traditionellem Erbe würden wir einen Zeiger auf eine Basisklasse wie diese verwenden:
void f (Drawable const * d) {
d-> draw (std::cout);
}
f ( new Square{}); Bei Dyno sind Polymorphismus und Wertsemantik kompatibel, und die Art und Weise, wie polymorphe Typen umgehen, kann hoch angepasst werden. Dazu müssen wir einen Typ definieren, der alles aufbewahren kann, was Drawable kann. Es ist dieser Typ anstelle eines Drawable* , dass wir zu und nach polymorphen Funktionen weitergeben werden. Um diesen Wrapper zu definieren, liefert Dyno den dyno::poly Container, der ein willkürliches Objekt aufnehmen kann, das ein bestimmtes Konzept erfüllt. Wie Sie sehen werden, spielt dyno::poly eine doppelte Rolle: Es speichert das polymorphe Objekt und kümmert sich um die dynamische Versendung von Methoden. Alles, was Sie tun müssen, ist eine dünne Wrapper über dyno::poly zu schreiben, um ihm genau die gewünschte Schnittstelle zu geben:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable> poly_;
};Hinweis: Sie können technisch gesehen direkt in Ihren Schnittstellen
dyno::polyverwenden. Es ist jedoch viel bequemer, einen Wrapper mit echten Methoden zu verwenden alsdyno::poly, und so wird das Schreiben einer Wrapper empfohlen.
Lassen Sie uns das niederlassen. Erstens definieren wir ein Mitglied poly_ , das ein polymorpher Behälter für alles ist, was das Drawable Konzept modelliert:
dyno::poly<Drawable> poly_; Dann definieren wir einen Konstruktor, der es ermöglicht, diesen Container aus einem willkürlichen Typ T zu konstruieren:
template < typename T>
drawable (T x) : poly_{x} { } Die unausgesprochene Annahme hier ist, dass T tatsächlich das Drawable Konzept modelliert. Wenn Sie ein dyno::poly aus einem Objekt vom Typ T erstellen, wird der Dyno die Konzeptkarte betrachten, die für Drawable und T definiert ist, falls vorhanden. Wenn es keine solche Konzeptkarte gibt, berichtet die Bibliothek, dass wir versuchen, einen dyno::poly aus einem Typ zu erstellen, der es nicht unterstützt, und Ihr Programm wird nicht kompilieren.
Schließlich ist der seltsamste und wichtigste Teil der obigen Definition der der draw :
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); } Was hier passiert, ist, dass wir, wenn .draw auf unser drawable Objekt aufgerufen wird, tatsächlich einen dynamischen Versand für die Implementierung der "draw" -Funktion für das derzeit im dyno::poly gespeicherte Objekt durchführen und das nennen. Um nun eine Funktion zu erstellen, die alles akzeptiert, was Drawable , müssen sich keine Sorgen um Zeiger und Eigentum an Ihrer Benutzeroberfläche machen:
void f (drawable d) {
d. draw (std::cout);
}
f (Square{});Übrigens, wenn Sie denken, dass dies alles dumm ist und Sie eine Vorlage verwenden sollen, haben Sie Recht. Betrachten Sie jedoch Folgendes, wo Sie wirklich Laufzeit -Polymorphismus benötigen:
drawable get_drawable () {
if ( some_user_input ())
return Square{};
else
return Circle{};
}
f (get_drawable()); Streng genommen müssen Sie dyno::poly nicht wickeln, aber dies gibt eine schöne Barriere zwischen Dyno und dem Rest Ihres Codes, was sich nie darum kümmern muss, wie Ihre polymorphe Schicht implementiert wird. Außerdem haben wir weitgehend ignoriert, wie dyno::poly in der obigen Definition implementiert wurde. dyno::poly ist jedoch ein sehr leistungsstarker politischen Container für polymorphe Objekte, der sich an die Bedürfnisse für die Leistung angepasst werden kann. Durch das Erstellen eines drawable Wrappers können Sie die von dyno::poly für Leistung verwendete Implementierungsstrategie für die Leistung leicht optimieren, ohne den Rest Ihres Codes zu beeinflussen.
Der erste Aspekt, der in einem dyno::poly angepasst werden kann, ist die Art und Weise, wie das Objekt im Behälter gespeichert wird. Standardmäßig speichern wir einfach einen Zeiger auf das tatsächliche Objekt, wie man mit polymorphischer Vererbung basiert. Dies ist jedoch oft nicht die effizienteste Implementierung, und deshalb ermöglicht dyno::poly das Anpassen. Übergeben Sie dazu einfach eine Speicherrichtlinie an dyno::poly . Lassen Sie uns beispielsweise unsere drawable Wrapper so definieren, dass sie versucht, Objekte bis zu 16 Bytes in einem lokalen Puffer zu speichern, fällt dann aber auf den Haufen zurück, wenn das Objekt größer ist:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
dyno::poly<Drawable, dyno::sbo_storage< 16 >> poly_;
// ^^^^^^^^^^^^^^^^^^^^^ storage policy
}; Beachten Sie, dass nichts außer der Richtlinie in unserer Definition geändert wurde. Das ist ein sehr wichtiger Grundsatz von Dyno ; Diese Richtlinien sind Implementierungsdetails und sollten die Art und Weise, wie Sie Ihren Code schreiben, nicht ändern. Mit der obigen Definition können Sie jetzt genauso wie zuvor drawable S erstellen, und es wird keine Zuordnung erfolgen, wenn das Objekt, das Sie aus Anpassungen in 16 Bytes erstellen, die drawable erstellen. Wenn es jedoch nicht passt, verteilt dyno::poly einen groß genug großen Puffer auf dem Haufen.
Nehmen wir an, Sie möchten eigentlich nie eine Zuteilung durchführen. Kein Problem, ändern Sie einfach die Politik in dyno::local_storage<16> . Wenn Sie versuchen, ein drawable aus einem Objekt zu erstellen, das zu groß ist, um in den lokalen Speicher zu passen, wird Ihr Programm nicht kompilieren. Wir speichern nicht nur eine Zuordnung, sondern sparen auch jedes Mal, wenn wir auf das polymorphe Objekt zugreifen, eine Zeigerindirektion, wenn wir uns mit dem traditionellen Ansatz basieren. Wenn Sie diese (wichtigen) Implementierungsdetails für Ihren speziellen Anwendungsfall optimieren, können Sie Ihr Programm viel effizienter machen als mit klassischer Vererbung.
Andere Speicherrichtlinien werden ebenfalls bereitgestellt, wie dyno::remote_storage und dyno::non_owning_storage . dyno::remote_storage ist die Standardeinstellung, die immer einen Zeiger auf ein heap-zualloziertes Objekt speichert. dyno::non_owning_storage speichert einen Zeiger auf ein bereits existierendes Objekt, ohne sich über die Lebensdauer dieses Objekts zu sorgen. Es ermöglicht die Implementierung von nicht besitzenden polymorphen Ansichten über Objekte, was sehr nützlich ist.
Benutzerdefinierte Speicherrichtlinien können auch ganz einfach erstellt werden. Einzelheiten siehe <dyno/storage.hpp> .
Als wir dyno::poly vorstellten, erwähnten wir, dass es zwei Rollen hatte; Das erste besteht darin, das polymorphe Objekt zu speichern, und das zweite besteht darin, einen dynamischen Versand durchzuführen. Genau wie der Speicher angepasst werden kann, kann auch die dynamische Versendung durchgeführt werden, die auch mit Richtlinien angepasst werden kann. Lassen Sie uns beispielsweise unseren drawable Wrapper so definieren, dass er stattdessen den VTABLE im drawable Objekt selbst speichert, anstatt einen Zeiger auf die VTABLE zu speichern. Auf diese Weise vermeiden wir jedes Mal eine Indirektion, wenn wir auf eine virtuelle Funktion zugreifen:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >; // storage policy
using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
dyno::poly<Drawable, Storage, VTable> poly_;
}; Beachten Sie, dass sich nichts außer der vtable Richtlinie in der Definition unseres drawable Typs ändern muss. Wenn wir wollten, könnten wir die Speicherrichtlinie unabhängig von der VTABLE -Richtlinie ändern. Mit dem oben genannten, obwohl wir alle Indirektionen sparen, zahlen wir dafür, indem wir unser drawable Objekt größer machen (da es die VTABLE vor Ort halten muss). Dies könnte unerschwinglich sein, wenn wir viele Funktionen im VTABLE hätten. Stattdessen wäre es sinnvoller, den größten Teil der vtable remote zu speichern, aber nur die wenigen Funktionen, die wir stark nennen. Dyno macht es sehr einfach, dies durch Verwendung von Selektoren zu tun, mit denen anpassen kann, welche Funktionen eine Richtlinie gilt:
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out); }
private:
using Storage = dyno::sbo_storage< 16 >;
using VTable = dyno::vtable<
dyno::local<dyno::only<decltype( " draw " _s)>>,
dyno::remote<dyno::everything_else>
>;
dyno::poly<Drawable, Storage, VTable> poly_;
}; Angesichts dieser Definition wird der VTable tatsächlich in zwei Teile geteilt. Der erste Teil ist lokal zum drawable Objekt und enthält nur die draw . Der zweite Teil ist ein Zeiger auf einen VTable in statischer Speicher, der die verbleibenden Methoden enthält (z. B. Destruktor).
Dyno liefert zwei vtbare Richtlinien, dyno::local<> und dyno::remote<> . Beide Richtlinien müssen mit einem Selektor angepasst werden. Die von der Bibliothek unterstützten Selektoren sind dyno::only<functions...> , dyno::except<...> und dyno::everything_else (was auch dyno::everything geschrieben werden kann).
Bei der Definition eines Konzepts kann man häufig eine Standarddefinition für zumindest einige mit dem Konzept zugeordnete Funktionen liefern. Beispielsweise wäre es standardmäßig sinnvoll, eine Mitgliedsfunktion mit dem Namen draw (falls vorhanden) zu verwenden, um die abstrakte "draw" -Methode des Drawable Konzepts zu implementieren. Dafür kann man dyno::default_concept_map verwenden:
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = []( auto const & self, std::ostream& out) { self. draw (out); }
); Immer wenn wir versuchen, zu prüfen, wie ein Typ T das Drawable Konzept erfüllt, werden wir auf die Standardkonzeptkarte zurückreichen, wenn keine Konzeptkarte definiert wurde. Zum Beispiel können wir einen neuen Circle erstellen:
struct Circle {
void draw (std::ostream& out) const {
out << " circle " << std::endl;
}
};
f (Circle{}); // prints "circle" Circle ist automatisch ein Modell der Drawable , obwohl wir eine Konzeptkarte für Circle nicht explizit definiert haben. Wenn wir dagegen eine solche Konzeptkarte definieren würden, hätte dies Vorrang vor dem Standard:
template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
" draw " _s = [](Circle const & circle, std::ostream& out) {
out << " triangle " << std::endl;
}
);
f (Circle{}); // prints "triangle" Es ist manchmal nützlich, eine Konzeptkarte für eine vollständige Familie von Typen auf einmal zu definieren. Zum Beispiel möchten wir std::vector<T> zu einem Modell der Drawable machen, aber nur, wenn T auf einen Stream gedruckt werden kann. Dies ist leicht zu erreichen, indem Sie diesen (nicht so) geheimen Trick verwenden:
template < typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std:: void_t <decltype(
std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
" draw " _s = [](std::vector<T> const & v, std::ostream& out) {
for ( auto const & x : v)
out << x << ' ' ;
}
);
f (std::vector< int >{ 1 , 2 , 3 }) // prints "1 2 3 "Beachten Sie, wie wir
std::vectorüberhaupt nicht ändern müssen. Wie können wir das mit klassischem Polymorphismus tun? Antwort: Nein kann es tun.
Dyno ermöglicht das Löschen von Nichtmitgliedfunktionen und -funktionen, die auch in einem willkürlichen Argument (aber nur einem Argument) versandt werden. Definieren Sie dazu einfach das Konzept mit dyno::function anstelle von dyno::method und verwenden Sie den dyno::T -Platzhalter, um das ausgelöste Argument zu bezeichnen:
// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (dyno::T const &, std::ostream&)>
)) { }; Der oben verwendete dyno::T const& Parameter repräsentiert den Typ des Objekts, auf dem die Funktion aufgerufen wird. Es muss jedoch nicht der erste Parameter sein:
struct Drawable : decltype(dyno::requires_(
" draw " _s = dyno::function< void (std::ostream&, dyno::T const &)>
)) { };Die Erfüllung des Konzepts ändert sich nicht, ob das Konzept eine Methode oder eine Funktion verwendet, sondern stellen Sie sicher, dass die Parameter Ihrer Funktionsimplementierung der der im Konzept deklarierten Funktion übereinstimmen:
// Define how concrete types can fulfill that interface
template < typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
" draw " _s = [](std::ostream& out, T const & self) { self. draw (out); }
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
); Wenn Sie schließlich eine function auf einem dyno::poly aufrufen, müssen Sie alle Parameter explizit eingeben, da Dyno nicht erraten kann, auf welchen Sie entsenden möchten. Der Parameter, der mit einem dyno::T -Platzhalter in dem Konzept deklariert wurde, sollte übergeben werden. Der dyno::poly selbst:
// Define an object that can hold anything that can be drawn.
struct drawable {
template < typename T>
drawable (T x) : poly_{x} { }
void draw (std::ostream& out) const
{ poly_. virtual_ ( " draw " _s)(out, poly_); }
// ^^^^^ passing the poly explicitly
private:
dyno::poly<Drawable> poly_;
};