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.

In Pocket speichern vorlesen Druckansicht
Lesezeit: 6 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

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.

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.

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.


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. ()