Ein Interface ist ein Vertrag zwischen einem Benutzer und einem Implementierer. Die C++ Core Guidelines enthalten wertvolle Regeln, die solche Schnittstellen betreffen.
Bevor dieser Artikel in die Tiefe taucht, stehen einige Begriffsklärungen an. Bei Funktionsobjekten handelt es sich um Klassen mit überladenem Aufrufoperator. In der C++-Community hat es sich eingebürgert, sie Funktoren zu nennen, doch das ist falsch: Dieser Begriff stammt aus der Algebra [1]. Die automatische Bestimmung der Template-Argumente wird meist kurz Template Argument Deduction genannt. Nun ist es aber an der Zeit, in die Tiefe zu tauchen.
Überall Funktionsobjekte
Die Regel T.40 [2] lautet: “Use function objects to pass operations to algorithms.” Oft lässt sich das Verhalten der rund 100 Algorithmen der STL anpassen, indem man ein Callable verwendet. Dabei handelt es sich in der Regel um Funktionen, Funktionsobjekte oder Lambdas. Listing 1 zeigt drei Möglichkeiten, einen Vektor von Strings zu sortieren.
Listing 1
Sortieren eines Strings
#include <algorithm>
#include <functional>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>
bool byLessLength(const std::string& f, const std::string& s) {
return f.size() < s.size();
}
class ByGreaterLength {
public:
bool operator()(const std::string& f, const std::string& s) const {
return f.size() > s.size();
}
};
int main() {
std::vector<std::string> myStrVec = {"523345", "4336893456", "7234", "564", "199", "433", "2435345"};
std::cout << '\n';
std::cout << "Ascending by length with a function \n";
std::sort(myStrVec.begin(), myStrVec.end(), byLessLength);
for (const auto& str: myStrVec) std::cout << str << " ";
std::cout << "\n\n";
std::cout << "Descending by length with a function object \n";
std::sort(myStrVec.begin(), myStrVec.end(), ByGreaterLength());
for (const auto& str: myStrVec) std::cout << str << " ";
std::cout << "\n\n";
std::cout << "Ascending by length with a lambda \n";
std::sort(myStrVec.begin(), myStrVec.end(), [](const std::string& f, const std::string& s) {
return f.size() < s.size();
});
for (const auto& str: myStrVec) std::cout << str << " ";
std::cout << "\n\n";
}
Das Programm sortiert einen Vektor von Strings auf der Grundlage der Länge der Strings. Dabei kommt der Algorithmus »std::sort« [3] zum Einsatz. Die Zeilen 23, 27 und 31 verwenden eine Funktion, ein Funktionsobjekt und einen Lambda-Ausdruck. Als Funktionsobjekt bezeichnet man eine Klasse (Zeile 12), für die der Aufrufoperator (»operator()«) überladen ist. Abbildung 1 zeigt die Ausgabe des Programms.
Die Regel besagt, dass man Funktionsobjekte als Operationen für Algorithmen verwendeen soll. Was sind nun die Vorteile solcher Funktionsobjekte? Die Antwort lautet kurz und bündig: Performanz, Ausdruckskraft und Zustand. Freilich handelt es sich bei Lambda-Ausdrücken nur um verkleidete Funktionsobjekte.
Je mehr der Optimierer lokal argumentieren kann, desto performanter fällt der Code aus. Ein Lambda (Zeile 31) generiert der Compiler einfach an Ort und Stelle. Das unterscheidet sich deutlich von einer Funktion, die in einer anderen Übersetzungseinheit definiert sein kann. In diesem Fall kann der Optimierer nicht alle Optimierungsschritte vornehmen.
Der Code des Funktionsobjekts sollte so ausdrucksstark sein, dass er keine Dokumentation benötigt. Lambdas bieten diese Ausdruckskraft, da man sie in der Regel an der Stelle definiert, an der sie auch verwendet werden.
Im Gegensatz zu einer Funktion kann ein Funktionsobjekt einen Zustand besitzen. Listing 2 verdeutlicht diese Beobachtung. Der Aufruf »std::for_each« in Zeile 19 ist entscheidend für das Programm. Bei »std::for_each« [4] handelt es sich um einen einzigartigen Algorithmus der Standard Template Library, weil er sein Callable zurückgeben kann.
Listing 2
Aufsummieren eines Vektors
#include <algorithm>
#include <iostream>
#include <vector>
class SumMe {
int sum{0};
public:
SumMe() = default;
void operator()(int x) {
sum += x;
}
int getSum() const {
return sum;
}
};
int main() {
std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
SumMe sumMe = std::for_each(intVec.begin(), intVec.end(), SumMe());
std::cout << '\n';
std::cout << "Sum of intVec= " << sumMe.getSum() << '\n';
std::cout << '\n';
}
Im Listing erhält »std::for_each« das Funktionsobjekt »SumMe« und kann daher das Ergebnis des Funktionsaufrufs direkt im Funktionsobjekt speichern. In Zeile 21 erfolgt die Frage nach der Summe aller Aufrufe. Abbildung 2 stellt das Ergebnis der Summation dar.
Der Vollständigkeit halber präsentiert Listing 3, dass Lambdas auch einen Zustand besitzen und sich daher zum Akkumulieren von Werten verwenden lassen. Dieses Lambda sieht wohl ein wenig beängstigend aus. Zunächst einmal stellt die Variable »sum« den Zustand des Lambda dar. Mit C++14 lässt sich die Variable »sum« direkt definieren und intialisieren: »sum = 0«. Damit gilt »sum« nur im Bereich des Lambda. Lambdas sind per Default konstant. Durch die Deklaration des Lambda als »mutable« lässt sich jedoch der Wert der Variable »sum« verändern. Abbildung 3 zeigt die Ausgabe des Programms.
Listing 3
Aufsummieren eines Vektors per Lambda
#include <algorithm>
#include <iostream>
#include <vector>
int main(){
std::cout << '\n';
std::vector<int> intVec{1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
std::for_each(
intVec.begin(), intVec.end(),
[sum = 0](int i) mutable {
sum += i;
std::cout << sum << " ";
}
);
std::cout << "\n\n";
}
Ein Lambda-Ausdruck ist lediglich Syntactic Sugar [5] für ein Funktionsobjekt, das man an Ort und Stelle instanziiert und verwendet. Dank C++-Insights [6] lässt sich dieser Prozess schön visualisieren.
Seit dem ersten C++-Standard C++98 können Funktions-Templates per Default ihre Template-Argumente automatisch bestimmen. Mit C++17 klappt das auch für Klassen-Templates. Man sollte wenn irgend möglich die Regel T.44 [7] anwenden: “Use function templates to deduce class template argument types (where feasible).”
Alles wird einfacher
C++ bietet Fabrikfunktionen wie »std::make_tuple« oder »std::make_unique« hauptsächlich deswegen an, weil ein Funktions-Template seine Template-Argumente aus seinen Funktionsargumenten ableiten kann. Während dieses Prozesses wendet der Compiler einige einfache Konvertierungen an wie das Entfernen des äußersten »const«/»volatile«-Qualifizierers. Zudem vereinfacht er C-Arrays und Funktionen in einen Zeiger auf das erste Element des C-Arrays oder auf die Funktion.
Dieses automatische Ableiten von Template-Argumenten macht dem Programmierer das Leben sehr viel komfortabler. Statt die umständliche Definition aus der zweiten Zeile von Listing 4 zu tippen, kann er die Fabrikfunktion »std::make_tuple« verwenden (Zeile 4).
Listing 4
Einfache Definitionen
# umständlich
std::tuple<int, double, std::string> myTuple = {2011, 20.11, "C++11"};
# einfach
auto myTuple = std::make_tuple(2011, 20.11, "C++11");
# einfach (C++17)
std::tuple myTuple = {2017, 20.17, "C++17"};
Seit C++17 kann der Compiler in vielen Situationen seine Template-Argumente nicht nur aus den Funktionsargumenten, sondern auch aus den Konstruktorargumenten ableiten. Die letzte Zeile von Listing 4 demonstriert, wie sich »myTuple« in C++17 einfach definieren lässt. Eine offensichtliche Auswirkung dieses C++17-Features: Es macht die meisten Fabrikfunktionen wie »std::make_tuple« obsolet.
Das Programm aus Listing 5 zeigt die automatische Template-Argument-Bestimmung von Klassen- und Funktionsargumenten in Aktion. Die Kommentare verdeutlichen die explizite Spezifikation der Template-Argumente. Mit deren automatischer Bestimmung ruft der Benutzer nur noch eine Funktion oder eine Klasse auf. Die Tatsache, dass es sich bei der Funktion um ein Funktions-Template beziehungsweise bei der Klasse um ein Klassen-Template handelt, gerät damit zum reinen Implementierungsdetail. Abbildung 4 zeigt die Ausgabe des Programms.
Listing 5
Bestimmen der Template-Argumente
#include <iostream>
template <typename T>
void showMe(const T& t) {
std::cout << t << '\n';
}
template <typename T>
struct ShowMe{
ShowMe(const T& t) {
std::cout << t << '\n';
}
};
int main() {
std::cout << '\n';
showMe(5.5); // not showMe<double>(5.5);
showMe(5); // not showMe<int>(5);
ShowMe a(5.5); // not ShowMe<double>(5.5);
ShowMe b(5); // not ShowMe<int>(5);
std::cout << '\n';
}
Wie geht es weiter?
Templates kommen gern zum Einsatz, wenn die Variation von Algorithmen im Fokus steht. Genau dieser Herausforderung widmet sich der nächste Artikel. (jcb/jlu)
Infos
- Funktoren: https://de.wikipedia.org/wiki/Funktor_(Mathematik)
- T.40: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-fo
- »std::sort«: https://en.cppreference.com/w/cpp/algorithm/sort
- »std::for_each«: https://en.cppreference.com/w/cpp/algorithm/for_each
- Syntactic Sugar: https://en.wikipedia.org/wiki/Syntactic_sugar
- C++ Insights: https://cppinsights.io/
- T.44: https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rt-deduce









