C++ Core Guidelines:Type Erasure

C++ Core Guidelines:Type Erasure

Regel "T.5:Kombiniere generische und OO-Techniken, um ihre Stärken zu verstärken, nicht ihre Kosten" der Kernrichtlinien für generische Programmierung verwendet Typlöschung als Beispiel . Typ löschen? Wirklich! Natürlich benötige ich zwei Posts, um diese fortgeschrittene Template-Technik zu erklären.

Zunächst einmal:Was bedeutet Typlöschung?

  • Geben Sie Löschung ein: Type Erasure ermöglicht es Ihnen, verschiedene konkrete Typen über eine einzige generische Schnittstelle zu verwenden.

Natürlich haben Sie in C++ oder C bereits recht häufig Typlöschung verwendet. Die C-ähnliche Art der Typlöschung ist ein void-Zeiger; Die C++-ähnliche Art der Typlöschung ist die Objektorientierung. Beginnen wir mit einem void-Zeiger.

Void-Zeiger

Schauen wir uns die Deklaration von std::qsort genauer an :

void qsort(void *ptr, std::size_t count, std::size_t size, cmp);

mit:

int cmp(const void *a, const void *b);

Die Vergleichsfunktion cmp sollte ein

zurückgeben
  • negative ganze Zahl:das erste Argument ist kleiner als das zweite
  • null:beide Argumente sind gleich
  • positive ganze Zahl:das erste Argument ist größer als das zweite

Dank des Void-Zeigers std::qsort ist allgemein anwendbar, aber auch recht fehleranfällig.

Vielleicht möchten Sie eine std::vector<int>, sortieren aber Sie haben einen Komparator für C-Saiten verwendet. Der Compiler kann diesen Fehler nicht abfangen, da die Typinformationen entfernt wurden. Sie enden mit undefiniertem Verhalten.

In C++ können wir es besser machen:

Objektorientierung

Hier ein einfaches Beispiel, das als Ausgangspunkt für weitere Variationen dient.

// typeErasureOO.cpp

#include <iostream>
#include <string>
#include <vector>

struct BaseClass{ // (2)
 virtual std::string getName() const = 0;
};

struct Bar: BaseClass{ // (4)
 std::string getName() const override {
 return "Bar";
 }
};

struct Foo: BaseClass{ // (4)
 std::string getName() const override{
 return "Foo";
 }
};

void printName(std::vector<const BaseClass*> vec){ // (3)
 for (auto v: vec) std::cout << v->getName() << std::endl;
}


int main(){
 
 std::cout << std::endl;
 
 Foo foo;
 Bar bar; 
 
 std::vector<const BaseClass*> vec{&foo, &bar}; // (1)
 
 printName(vec);
 
 std::cout << std::endl;

}

std::vector<const Base*> (1) hat einen Zeiger auf eine Konstante BaseClasses . BaseClass ist eine abstrakte Basisklasse, die in (3) verwendet wird. Foo und Bar (4) sind die konkreten Klassen.

Die Ausgabe des Programms ist nicht so berauschend.

Um es formaler zu sagen. Foo und Bar Implementieren Sie die Schnittstelle des BaseClass und kann daher anstelle von BaseClass. verwendet werden Dieses Prinzip wird als Liskov-Substitutionsprinzip bezeichnet und ist Typlöschung in OO.

In der objektorientierten Programmierung implementieren Sie eine Schnittstelle. In dynamisch typisierten Sprachen wie Python interessieren Sie sich nicht für Schnittstellen, sondern für Verhalten.

Vorlagen

Lassen Sie mich einen kleinen Umweg machen.

In Python kümmern Sie sich um das Verhalten und nicht um formale Schnittstellen. Diese Idee ist als Duck Typing bekannt. Der Ausdruck geht, um es kurz zu machen, auf das Gedicht von James Whitcomb Rileys zurück:Hier ist es:

"Wenn ich einen Vogel sehe, der wie eine Ente läuft und wie eine Ente schwimmt und wie eine Ente quakt, nenne ich diesen Vogel eine Ente."

Was bedeutet das? Stellen Sie sich eine Funktion acceptOnlyDucks vor das akzeptiert nur Enten als Argument. In statisch typisierten Sprachen wie C++ sind alle abgeleiteten Typen Duck kann verwendet werden, um die Funktion aufzurufen. In Python alle Typen, die sich wie Duck verhalten 's, kann verwendet werden, um die Funktion aufzurufen. Um es konkreter zu machen. Wenn sich ein Vogel wie Duck verhält es ist ein Duck . In Python wird oft ein Sprichwort verwendet, um dieses Verhalten recht gut zu beschreiben.

Bitte nicht um Erlaubnis, bitte um Verzeihung.

Im Fall unserer Ente bedeutet dies, dass Sie die Funktion acceptsOnlyDucks aufrufen mit einem Vogel und hoffe das Beste. Wenn etwas Schlimmes passiert, fangen Sie die Ausnahme mit einer Ausnahmeklausel ab. Oft funktioniert diese Strategie sehr gut und sehr schnell in Python.

Okay, das ist das Ende meines Umweges. Vielleicht fragen Sie sich, warum ich in diesem C++-Beitrag über Ententypisierung geschrieben habe. Der Grund ist ganz einfach. Dank Templates haben wir Duck Typing in C++. Wenn Sie Ententypisierung mit OO kombinieren, wird es sogar typsicher.

std::function als polymorpher Funktionswrapper ist ein nettes Beispiel für Typlöschung in C++.

std::function

std::function kann alles annehmen, was sich wie eine Funktion verhält. Präziser sein. Dies kann ein beliebiger Aufruf sein, wie eine Funktion, ein Funktionsobjekt, ein von std::bind erstelltes Funktionsobjekt , oder einfach nur eine Lambda-Funktion.

// callable.cpp

#include <cmath>
#include <functional>
#include <iostream>
#include <map>

double add(double a, double b){
 return a + b;
}

struct Sub{
 double operator()(double a, double b){
 return a - b;
 }
};

double multThree(double a, double b, double c){
 return a * b * c;
}

int main(){
 
 using namespace std::placeholders;

 std::cout << std::endl;

 std::map<const char , std::function<double(double, double)>> dispTable{ // (1)
 {'+', add }, // (2)
 {'-', Sub() }, // (3)
 {'*', std::bind(multThree, 1, _1, _2) }, // (4)
 {'/',[](double a, double b){ return a / b; }}}; // (5)

 std::cout << "3.5 + 4.5 = " << dispTable['+'](3.5, 4.5) << std::endl;
 std::cout << "3.5 - 4.5 = " << dispTable['-'](3.5, 4.5) << std::endl;
 std::cout << "3.5 * 4.5 = " << dispTable['*'](3.5, 4.5) << std::endl;
 std::cout << "3.5 / 4.5 = " << dispTable['/'](3.5, 4.5) << std::endl;

 std::cout << std::endl;

}

In diesem Beispiel verwende ich eine Dispatch-Tabelle (1), die Zeichen Callables zuordnet. Ein Callable kann eine Funktion (1), ein Funktionsobjekt (2), ein von std::bind erstelltes Funktionsobjekt sein (3) oder eine Lambda-Funktion. Der Schlüsselpunkt von std::function ist, dass es alle verschiedenen Funktionstypen akzeptiert und löscht ihre Typen. std::function verlangt von seinen Callables, dass es zwei double's nimmt und gibt ein double: std::function<double(double, double)>. zurück

Um das Beispiel zu vervollständigen, hier ist die Ausgabe.

Bevor ich im nächsten Beitrag mehr über das Löschen von Typen mit Templates schreibe, möchte ich die drei Techniken zum Implementieren des Löschens von Typen zusammenfassen.

Sie können das Löschen von Typen mit Void-Zeigern, Objektorientierung oder Vorlagen implementieren. Lediglich die Implementierung mit Templates ist typsicher und benötigt keine Typhierarchie. Die fehlenden Details zu den Vorlagen folgen.

Was kommt als nächstes?

Ich nehme an, Sie wollen wissen, wie das Löschen von Typen mit Vorlagen implementiert wird? Natürlich musst du auf meinen nächsten Beitrag warten.