Von Variadic Templates zu Fold Expressions
Nach den Beiträgen "Variadic Templates oder die Power der drei Punkte" und "Mehr über Variadic Templates ... " geht es einen Schritt weiter in die Zukunft mit Fold Expressions, die ein Parameterpack direkt mit einem binären Operator reduzieren können.
- Rainer Grimm
Nach den Beiträgen "Variadic Templates oder die Power der drei Punkte" und "Mehr über Variadic Templates ..." geht es einen Schritt weiter in die Zukunft mit Fold Expressions, die ein Parameterpack direkt mit einem binären Operator reduzieren können.
Ich habe absichtlich einen wichtigen Anwendungsfall für Variadic Templates ausgelassen: Wer Perfect Forwarding mit Variadic Templates kombiniert, erhält die perfekte Fabrikfunktion, eine Funktion, die eine beliebige Anzahl von Argumenten beliebiger Wertkategorie (lWert oder rWert) annehmen kann. Neugierig? Mehr findet sich in meinen vorherigen Beitrag "C++ Core Guidelines: Regeln für Variadische Templates".
Jetzt möchte ich etwas Neues vorstellen: Fold Expressions.
Fold Expressions
C++11 unterstützt Variadic Templates. Das sind Templates, die eine beliebige Anzahl von Template-Argumenten annehmen können. Die beliebige Anzahl wird in einem sogenannten Parameterpack gebunden. Mit C++17 gibt es außerdem Fold Expressions, mit denen sich ein Parameterpack direkt mit einem binären Operator reduzieren lässt. Folgender Code reduziert beliebig viele Zahlen auf einen Wert:
// variadicTemplatesFoldExpression.cpp
#include <iostream>
bool allVar() { // (1)
return true;
}
template<typename T, typename ...Ts> // (2)
bool allVar(T t, Ts ... ts) { // (3)
return t && allVar(ts...); // (4)
}
template<typename... Args> // (5)
bool all(Args... args) { return (... && args); }
int main() {
std::cout << std::boolalpha;
std::cout << '\n';
std::cout << "allVar(): " << allVar() << '\n';
std::cout << "all(): " << all() << '\n';
std::cout << "allVar(true): " << allVar(true) << '\n';
std::cout << "all(true): " << all(true) << '\n';
std::cout << "allVar(true, true, true, false): "
<< allVar(true, true, true, false) << '\n';
std::cout << "all(true, true, true, false): "
<< all(true, true, true, false) << '\n';
std::cout << '\n';
}
Die beiden Templates allVar
und all
geben zur Kompilierzeit zurück, wenn alle Argumente wahr sind. allVar
verwendet ein Variadic Template; all
Fold Expressions.
Das Variadic Template allVar
wendet Rekursion an, um seine Argumente auszuwerten. Die Funktion allVar
(1) ist die Randbedingung, falls das Parameterpack leer ist. In dem Funktions-Template allVar
(2) findet die Rekursion statt. Die drei Punkte stehen für das sogenannte Parameterpack. Diese unterstützen zwei Operationen. Man kann sie packen oder entpacken. Gepackt wird es in (2), entpackt in (3) und (4).
(4) erfordert unsere volle Aufmerksamkeit: Hier wird der Kopf des Parameterpakets t
mit dem Rest ts
des Parameterpacks ts mithilfe des binären Operators && kombiniert. Der Aufruf allVar(ts ...)
löst die Rekursion aus. Der Aufruf enthält ein Parameterpack, und zwar das ursprüngliche, das um den Kopf reduziert ist. Fold Expression (5) machen die Arbeit leichter. Mit Fold Expressions lässt das Parameterpack mithilfe des binären Operators direkt reduzieren: (... && args
).
Hier ist die Ausgabe des Programms:
Eine Fold Expression wendet einen binären Operator auf ein Parameterpack an.
Anwendung des binären Operators
Eine Fold Expression kann den binären Operator auf zwei verschiedene Arten anwenden.
- Mit und ohne einen Anfangswert.
- Prozessieren des Parameterpacks von links oder von rechts
Es gibt einen feinen Unterschied zwischen dem Algorithmus allVar
und all
. Letzterer besitzt den Standardwert true
für das leere Parameterpack.
C++17 unterstützt 32 binäre Operatoren in Fold Expressions: + - * / % ^ & | = < > << >> += -= *= /= %= ^= &= |= <<= >>= == != <= >= && || , .* ->*
. Einige von ihnen haben Default-Werte:
Binäre Operatoren, die keinen Standardwert haben, erfordern einen Anfangswert. Für binäre Operatoren, die einen Standardwert haben, ist der Anfangswert optional.
Wenn die Ellipse links vom Parameterpack steht, wird das Parameterpaket von links verarbeitet. Steht sie rechts davon, wird es von rechts verarbeitet. Das gilt auch, wenn ein Anfangswert zum Einsatz kommt.
Die Fold Expression ermöglicht es, Haskells Funktionen foldl
, foldr
, foldl1
und foldr1
direkt in C++ zu implementieren.
Ein Hauch von Haskell
Die folgende Tabelle zeigt die vier Varianten von Fold Expressions und ihre Haskell-Pendants. Der C++17-Standard schreibt vor, dass Fold Expresson mit Anfangswert den gleichen binären Operator op
verwenden.
Die C++- und Haskell-Varianten unterscheiden sich in zwei Punkten. Die C++-Version verwendet den Standardwert und die Haskell-Version das erste Element als Anfangswert. Die C++-Variante verarbeitet das Parameterpack zur Kompilierzeit, während die Haskell-Variante seine Liste zur Laufzeit verarbeitet.
Das folgende Programm zeigt alle vier Varianten der Fold Expression. Jede subtrahiert ein paar Zahlen.
// foldVariations.cpp
#include <iostream>
template<typename... Args> // (1)
auto diffL1(Args const&... args) {
return (... - args);
}
template<typename... Args> // (2)
auto diffR1(Args const&... args) {
return (args - ...);
}
template<typename Init, typename... Args> // (3)
auto diffL(Init init, Args const&... args) {
return (init - ... - args);
}
template<typename Init, typename... Args> // (4)
auto diffR(Init init, Args const&... args) {
return (args - ... - init);
}
int main() {
std::cout << '\n';
// (1 - 2) - 3:
std::cout << "diffL1(1, 2, 3): " << diffL1(1, 2, 3) << '\n';
// 1 - (2 - 3)
std::cout << "diffR1(1, 2, 3): " << diffR1(1, 2, 3) << '\n';
// ((10 - 1) - 2) - 3:
std::cout << "diffL(10, 1, 2, 3): " << diffL(10, 1, 2, 3) << '\n';
// 1 - (2 - (3 - 10)):
std::cout << "diffR(10, 1, 2, 3): " << diffR(10, 1, 2, 3) << '\n';
std::cout << '\n';
}
Die Funktionen diffL1
(1) und diffL
(3) verarbeiten die Zahlen von links und die Funktionen diffR1
(2) und diffR
(3) verarbeiten sie von rechts. Außerdem verwenden die Funktionen diffL
und diffR
einen Startwert. In den Kommentaren in der main
-Funktion habe ich die Verarbeitungsschritte dargestellt.
Ich führe das Haskell-Pendant direkt in der Haskell-Shell aus.
Wie geht's weiter?
Variadic Templates und vor allem Fold Expressions ermöglichen es, prägnante Ausdrücke für wiederholte Operationen zu schreiben. Mehr dazu in meinem nächsten Beitrag. ()