Wie schön die Features in modernem C++ ineinander greifen, zeigt mein Lieblingsbeispiel: Ein dispatch table mit modernem C++. Ein dispatch table ist eine Tabelle von Zeigern auf Funktionen. In meinen konkreten Fall ist es eine Tabelle von Verweisen auf polymorphe Funktionswrapper.
Doch zuerst einmal. Was meine ich mit modernem C++? In dem dispatch table kommen Feature aus C++11 zum Einsatz. Auf der Zeitachse habe ich aus C++14 dargestellt. Dazu aber später mehr.
Dispatch table
Das Beispiel zeigt ein einfaches dispatch table, das Zeichen auf Funktionsobjekte abbildet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// dispatchTable.cpp #include <cmath> #include <functional> #include <iostream> #include <map> int main(){ std::cout << std::endl; // dispatch table std::map<const char, std::function<double(double,double)> > dispTable; dispTable.insert( std::make_pair('+', [](double a, double b){ return a + b; })); dispTable.insert( std::make_pair('-', [](double a, double b){ return a - b; })); dispTable.insert( std::make_pair('*', [](double a, double b){ return a * b; })); dispTable.insert( std::make_pair('/', [](double a, double b){ return a / b; })); // do the math 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; // add a new operation dispTable.insert( std::make_pair('^', [](double a, double b){ return std::pow(a, b);} )); std::cout << "3.5^4.5= " << dispTable['^'](3.5, 4.5) << std::endl; std::cout << std::endl; }; |
Wie funktioniert das ganze? Das dispatch table ist eine std::map in, die Paaren const char und std::function<double(double,double) besitzt. Natürlich hätte ich auch anstelle des klassischen std::map ein neue std::unordered_map verwenden können. std::function ist ein sogenannter polymorpher Funktionswrapper. Als dieser kann er alles annehmen, was sich wie eine Funktion anfühlt. Dies kann eine Funktion, ein Funktionsobjekt oder auch wie in dem konkreten Beispiel (Zeile 14 - 17) eine Lambda-Funktion sein. Die einzige Bedingung, die std::function<double(double,double)> an seine Objekte stellt, ist, das sie zwei double Argumente erhalten und eine double Argument zurückgeben. Genau diese Bedingung erfüllen die Lambda-Funktionen.
In den Zeilen 20 - 23 kommen die Funktionsobjekte zum Einsatz. So gibt zum Beispiel der Aufruf dispTable['+'] in Zeile 20 das Funktionsobjekt zurück, dass mit der Lambda-Funktion [](double a, double b){ return a + b; } in Zeile 14 initialisiert wurde. Damit das Funktionsobjekt ausgeführt wird, benötigt es noch seine zwei Argumente. Diese kommen in dem Ausdruck dispTable['+'](3.5, 4.5) zum Einsatz.
Ein std::map ist eine dynamische Datenstruktur. Daher kann ich die Arithmetik einfach um die Operation '^' erweitern (Zeile 27) und anschließend verwenden. Zum Abschluss die ganze Rechnerei.
Eine kleine Erklärung bin ich noch schuldig geblieben. Warum ist das mein Lieblingsbeispiel?
Wie in Python
Ich halte häufig Python Schulungen. Eines meiner Beispiele, um den einfachen Umgang mit Python zu motivieren, ist ein dispatch table.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# dispatchTable.py dispTable={ "+": (lambda x, y: x+y), "-": (lambda x, y: x-y), "*": (lambda x, y: x*y), "/": (lambda x, y: x/y) } print print "3.5+4.5= ", dispTable['+'](3.5, 4.5) print "3.5-4.5= ", dispTable['-'](3.5, 4.5) print "3.5*4.5= ", dispTable['*'](3.5, 4.5) print "3.5/4.5= ", dispTable['/'](3.5, 4.5) dispTable['^']= lambda x, y: pow(x,y) print "3.5^4.5= ", dispTable['^'](3.5, 4.5) print |
Diese Implementierung basiert auf den funktionalen Featuren von Python. Dank std::map, std::function und Lambda-Funktion kann ich jetzt das fast gleiche Beispiel in C++11 verwenden, um die Mächtigkeit von C++ zu unterstreichen. Das hätte ich mir vor 10 Jahren noch nicht ausgemalt.
Generische Lambda-Funktionen
Fast hätte ich es vergessen. Mit C++14 werden Lambda-Funktionen noch mächtiger. Lambda-Funktionen können automatisch den Typ ihrer Argumente bestimmen. Grundlage der neuen Funktionalität sind Lambda-Funktionen und die automatische Typableitung mit auto. Es versteht sich von selbst, dass beide Feature Charakteristiken der funktionalen Programmierung sind.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// generalizedLambda.cpp #include <iostream> #include <string> #include <typeinfo> int main(){ std::cout << std::endl; auto myAdd= [](auto fir, auto sec){ return fir+sec; }; std::cout << "myAdd(1, 10)= " << myAdd(1, 10) << std::endl; std::cout << "myAdd(1, 10.0)= " << myAdd(1, 10.0) << std::endl; std::cout << "myAdd(std::string(1),std::string(10.0)= " << myAdd(std::string("1"),std::string("10")) << std::endl; std::cout << "myAdd(true, 10.0)= " << myAdd(true, 10.0) << std::endl; std::cout << std::endl; std::cout << "typeid(myAdd(1, 10)).name()= " << typeid(myAdd(1, 10)).name() << std::endl; std::cout << "typeid(myAdd(1, 10.0)).name()= " << typeid(myAdd(1, 10.0)).name() << std::endl; std::cout << "typeid(myAdd(std::string(1), std::string(10))).name()= " << typeid(myAdd(std::string("1"), std::string("10"))).name() << std::endl; std::cout << "typeid(myAdd(true, 10.0)).name()= " << typeid(myAdd(true, 10.0)).name() << std::endl; std::cout << std::endl; } |
In der Zeile 11 ist die generische Lambda-Funktion. Diese kann mit beliebigen Typen für ihre Argumente fir und sec aufgerufen werden und ermittelt als Lambda-Funktion ihren Rückgabetyp automatisch. Um sie in den folgen Zeilen verwenden zu können, binde ich sie an den Namen myAdd. Zeile 13 - 17 zeigen die Lambda-Funktion in der Anwendung. Natürlich interessiert mich, welchen Typ der Compiler als Rückgabetype ermittelt. Dazu verwende ich den typeid Operator in den Zeilen 21 - 25. Dieser setzt die Headerdatei <typeinfo> voraus.
Der typeid Operator ist nicht besonders zuverlässig. Er gibt ein C String zurück, der von der Implementierung abhängt. Weder sichert der Operator zu, dass der String unterschiedlich für verschiedene Typen ist, noch das der String für jeden Aufruf des Programms identisch ist. Für unsere einfache Anwendung ist die Ausgabe des typeid Operator aber ausreichend zuverlässig.
Mein Rechner mit meinem C++14 Compiler ist gerade in Reparatur, daher habe ich das Programm auf cppreference.com ausgeführt.
Schön zeigt die Ausgabe die verschiedenen Rückgabetypen. Die C-String i und d stehen für die Typen int und double. Lediglich der Typ des C++-Strings ist nicht besonders schön zu lesen. Es lässt sich aber zu mindestens erkennen, dass std::string ein Synonym für std::basic_string ist.
Wie geht's weiter?
Im nächsten Artikel werde ich ein wenig in die nahe und die ferne Zukunft von C++ aus einer funktionalen Perspektive blicken. Mit C++17 und C++20 werden die funktionalen Aspekte von C++ deutlich mächtiger.
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...