SFINAE-basierte Techniken
Content |
Bei SFINAE (substitution failure is not an error) schließen wir Kandidaten aus dem Kandidatenpool für bestimmte Template-Parameter aus, wenn gewisse Template-Abhängigkeiten nicht erfüllt sind.
Wie wir zuvor bereits gesehen haben, ist decltype hier ein sehr hilfreiches Werkzeug, da es nicht nur eingesetzt werden kann, um den Datentyp eines Ausdrucks zu bestimmen und an geeigneter Stelle einzufügen, sondern auch dazu genutzt werden kann, das Vorhandensein all der gewünschten Operatoren, Methoden und Funktionen sicherzustellen. Hier kommt diese Technik noch einmal zum Einsatz an einem etwas komplexeren Beispiel:
#ifndef SUM_HPP #define SUM_HPP template<typename IT> auto sum(IT begin_it, IT end_it) -> decltype(*begin_it + *begin_it++) { using T = decltype(*begin_it + *begin_it++); T sum{}; while (begin_it != end_it) { sum += *begin_it++; } return sum; } #endif
#include <iostream> #include "dim.hpp" #include "sum.hpp" int main() { int values[] = {2, 3, 5, 7, 11, 13}; std::cout << sum(values, values + dim(values)) << std::endl; }
theon$ g++ -Wall -o sum sum.cpp theon$ ./sum 41 theon$
Warum wurde hier bei decltype der Ausdruck *begin_it + *begin_it++ verwendet? Hätte es nicht auch *begin_it getan?
Wir gewinnen damit folgende Vorteile:
-
Wir testen alle genutzte Operatoren, also die Dereferenzierung mit *, das Inkrementieren des Iterators mit ++ (hier als Post-Inkrement) und das Addieren der einzelnen Werte.
-
Der Typ von *begin_it ist eine lvalue reference. Beim int-Array im obigen Beispiel wäre dies int&. Die Referenz kann aufwändig mit std::remove_reference entfernt werden. Durch die Addition erzeugen wir aber implizit vom Typ her eine rvalue, d.h. wir erhalten dann int statt int&.
Aufgabe
Entwickeln Sie eine Template-Funktion für das Skalarprodukt für zwei beliebige Container, die
-
von der zuvor entwickelten dim-Funktion unterstützt werden,
-
die gleiche Dimensionierung besitzen,
-
die durch-iterierbar sind und
-
deren Elemente miteinander multipliziert und addiert werden können.
Welches der vier Kriterien ist zur Übersetzzeit nicht immer überprüfbar? Wie könnte das zur Laufzeit überprüft werden?
Denken Sie daran, dass Sie bei decltype bequem mehrere Ausdrücke mit Hilfe des Komma-Operators verketten können. Der letzte Ausdruck bestimmt dann den Typ des decltype-Konstrukts.
Achten Sie darauf, dass sowohl Container wie std::vector als auch traditionelle Arrays unterstützt werden, d.h. Sie können sich nicht darauf verlassen, dass Methoden begin und end existieren. Eine Abhilfe bieten hier die Funktionen std::begin und std::end aus #include <iterator>, die sowohl für Objekte mit den entsprechenden Funktionen als auch Zeiger funktionieren.
Sie können sich überlegen, wie Sie durch die beiden Container durchiterieren. Wenn Sie möchten, können Sie darauf bestehen, dass Sie einen random access iterator haben. Prinzipiell ist es aber auch möglich, ohne dim und ohne einen random access iterator auszukommen.
Folgendes Testprogramm sollte dann funktionieren:
#include <iostream> #include <vector> #include "dotprod.hpp" int main() { std::vector<double> a = {2.0, 4.5, 7.0}; int b[] = {4, 5, 6}; std::cout << "a dot b = " << dotprod(a, b) << std::endl; }