Funktionsobjekte
Es lohnt sich, die neue Fassung von initGeMatrix noch einmal genau anzusehen:
template <typename T, typename Index, typename InitValue> void initGeMatrix(Index m, Index n, T *A, Index incRowA, Index incColA, InitValue initValue) { for (Index j=0; j<n; ++j) { for (Index i=0; i<m; ++i) { A[i*incRowA+j*incColA] = initValue(i, j, m, n); } } }
Es ist offensichtlich, dass ein Funktionstyp mit vier Parametern bei InitValue übergeben werden kann. Es ist aber darüber hinaus auch möglich, Objekte einer Klasse mit einem entsprechenden Funktions-Operator zu übergeben. Das war zuvor bei der sehr expliziten Typspezifikation für initValue als Funktionszeiger nicht möglich.
Objekte mit einem Funktionsoperator werden in C++ Funktionsobjekte genannt.
Wann sind Funktionsobjekte im Vergleich zu einfachen Funktionen sinnvoll? Das Problem ist, dass einfache Funktionen nur ihre Parameter auswerten können (und ggf. globale Datenstrukturen). Häufig besteht aber Anlass, eine Funktion in einem Kontext aufzurufen, d.h. unter Verwendung von Variablen, die nicht als Parameter übergeben werden und die auch nicht global gehalten werden sollen. Letzteres könnte uns auch später ein ernstes Problem in Bezug auf die thread safety geben.
Wie wichtig der Kontext sein kann, wird offenbar, wenn wir folgende Fassung von initGeMatrix auf Basis unserer neuen Funktion initGeMatrix umsetzen möchten:
template <typename T, typename Index> void initGeMatrix(Index m, Index n, T *A, Index incRowA, Index incColA) { std::random_device random; std::mt19937 mt(random()); std::uniform_real_distribution<T> uniform(-100, 100); for (Index j=0; j<n; ++j) { for (Index i=0; i<m; ++i) { A[i*incRowA+j*incColA] = uniform(mt); } } }
Das Problem ist hier, dass ein Pseudo-Zufallszahlengenerator mt angelegt und danach kontinuierlich verwendet wird. Dieser ist hier lokal und sollte keinesfalls global werden. Wie lässt sich das bei der Lösung mit dem initValue-Parameter erreichen? Nun, wenn wir statt einer einfachen Funktion ein Funktionsobjekt verwenden, dann kann dieses Objekt beliebig viel Status verwalten wie beispielsweise die Variablen mt und uniform:
template<typename T, typename Index> struct RandomValues { std::mt19937 mt; std::uniform_real_distribution<T> uniform; RandomValues() : mt(std::random_device()()), uniform(-100, 100) { } T operator()(Index i, Index j, Index m, Index n) { return uniform(mt); } };
So könnte dann eine Initialisierung aussehen:
initGeMatrix(A, RandomValues<double, std::size_t>());
Aufgaben
-
Wenn das RandomValues-Objekt in diesem Beispiel erzeugt wird, lebt es dann auf dem Heap oder ist es ein globales, lokales oder temporäres Objekt? Was ist es nach der Parameterübergabe in initGeMatrix? Wie erfolgt die Parameterübergabe?
-
Wieso wurde oben std::random_device()() verwendet? Wieso hat das Konstrukt zwei Klammernpaare? Woran scheitert folgende naheliegende Fassung?
template<typename T, typename Index> struct RandomValues { std::random_device random; std::mt19937 mt; std::uniform_real_distribution<T> uniform; RandomValues() : mt(random()), uniform(-100, 100) { } T operator()(Index i, Index j, Index m, Index n) { return uniform(mt); } };
-
Im Grunde genommen benötigt der Funktionsoperator nur noch die Parameter i und j. Auf m und n kann verzichtet werden, da diese Parameter, wenn sie denn benötigt werden, als Komponenten in der Klasse untergebracht werden können. Vereinfachen Sie die entsprechenden Schnittstellen.
-
Ergänzen Sie RandomValues um einen Konstruktor mit einem Seed-Wert, der eine reproduzierbare Zahlenfolge ermöglicht.
-
Schreiben Sie eine Klasse IncreasingRandomValues analog zu RandomValues, bei der die konsekutiv abgerufenen einzelnen Werte streng monoton steigen, ansonsten aber wie gehabt pseudo-zufällig bestimmt werden.