Funktional in C++: Dispatch table

Inhaltsverzeichnis[Anzeigen]

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.

timeline.FunktionalCpp11Cpp14

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.

dispatchTable

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.

dispatchTablePython 

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.

generalizedLambdaFunctions

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.

 

 

 

 

 

 

title page smalltitle page small 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.

Kommentar schreiben


Modernes C++

Abonniere den Newsletter

Inklusive zwei Artikel meines Buchs
Introduction und Multithreading

Beiträge-Archiv

Sourcecode

Neuste Kommentare