Clevere Tricks mit Parameterpacks und Fold Expressions

Modernes C++ Rainer Grimm  –  0 Kommentare

Als Vervollständigung zu den Beiträgen zu Variadic Templates und Fold Expressions gibt es clevere Tricks mit Parameterpacks und Fold Expressions.

Clevere Tricks mit Parameterpacks und Fold Expressions

Fold Expressions ermöglichen es, ein Parameterpack mit einem binären Operator zu reduzieren. Dank ihnen kann man prägnante Ausdrücke für wiederholte Operationen schreiben, beispielsweise eine print-Funktion oder eine push_back-Funktion , um Elemente in einen Vektor zu schieben. Beginnen möchte ich mit der print Funktion.

// printFoldExpressions.cpp

#include <iostream>
#include <string>

template<typename ... Args>
void printMe(Args&& ... args) {
(std::cout << ... << std::forward<Args>(args)) << '\n';
}

int main() {

std::cout << '\n';

std::cout << std::boolalpha;

printMe();
printMe("Rainer ", "Grimm");
printMe(true, " ", "+", " ",false, " = ", true + false);

std::cout << '\n';

}

Die Funktion printMe kann eine beliebige Anzahl von Argumenten annehmen. In der konkreten Funktion bedeutet das: kein Argumente, zwei C-Strings sowie ein paar Strings und Zahlen. Die printMe Funktion bestimmt automatisch deren Typen und zeigt sie an. Drei leistungsstarke C++-Techniken sind beteiligt.

Zum Schluss ist hier die Ausgabe des Programms.

Clevere Tricks mit Parameterpacks und Fold Expressions

Dank der Fold Expression lässt sich eine beliebige Anzahl von Argumenten in einen Vektor schieben.

// pushBackFoldExpressions.cpp

#include <iostream>
#include <string>
#include <vector>

using namespace std;

template<typename T, typename... Args>
void myPushBack(vector<T>& v, Args&&... args) {
(v.push_back(args), ...); // (1)
}

int main() {

std::cout << '\n';

std::vector<int> myIntVec;
myPushBack(myIntVec, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for (auto v : myIntVec) std::cout << v << ' ';

std::cout << "\n\n";

std::vector myDoubleVec{1.1, 2.2, 3.3}; // (2)
myPushBack(myDoubleVec, 4.4, 5.5, 6.6);
for (auto v: myDoubleVec) std::cout << v << ' ';

std::cout << "\n\n";

}

(1) und (2) sind besonders interessant. (2) schiebt die drei double-Werte in den Vektor. Mit C++17 kann der Compiler automatisch die Typen der Argumente ableiten. Der Ausdruck (v.push_back(args),...) schiebt die Elemente von rechts mit dem binären Kommaoperator (,) auf den Vektor. Da er assoziativ ist, lassen sich die Elemente auch von links drauf schieben: (..., v.push_back(args)). Ehrlich gesagt, sieht das ungewohnt aus. Deshalb bevorzuge ich die erste Variante.

Der folgende Screenshot zeigt die Ausgabe des Programms.

Clevere Tricks mit Parameterpacks und Fold Expressions


Jetzt möchte ich einen Schritt zurück von den Fold Expressions zu den Variadic Templates gehen und das Overload Pattern vorstellen. Das Overload Pattern ist eine clevere Methode, um mehrere Lambdas in ein Overload Set zu packen.

Overload Pattern

Ich will es kurz machen. Hier ist das Overload Pattern mit C++20 implementiert:

template<typename ... Ts> struct Overload : Ts ... { using Ts::operator() ... ; };

Was? Entschuldigung, mein Fehler. Ich sollte es richtig layouten.

template<typename ... Ts> 
struct Overload : Ts ... {
using Ts::operator() ... ;
};

Die struct Overload kann beliebig viele Basisklassen (Ts ...) besitzen. Sie leitet sich von jeder Klasse public ab und nimmt den Aufrufoperator (Ts::operator...) jeder Basisklasse in ihren Geltungsbereich auf.

Es gibt noch mehr über diese vier magischen Codezeilen zu erklären. Bevor ich das in meinem nächsten Beitrag tue, möchte ich das Overload Pattern verwenden, um die Typen der Ganzzahlliterale darzustellen. Das folgende Programm erfordert einen C++20-Compiler.

// overloadPattern.cpp

#include <iostream>

template<typename ... Ts>
struct Overload : Ts ... {
using Ts::operator() ...;
};


int main() {

std::cout << '\n';

auto TypeOfIntegral = Overload {
[](int) { return " int"; },
[](unsigned int) { return " unsigned int"; },
[](long int) { return " long int"; },
[](long long int) { return "long long int"; },
[](auto) { return "unbekannter Typ"; },
};

std::cout << "TypeOfIntegral(5): " << TypeOfIntegral(5) << '\n';
std::cout << "TypeOfIntegral(5u): " << TypeOfIntegral(5u) << '\n';
std::cout << "TypeOfIntegral(5U): " << TypeOfIntegral(5U) << '\n';
std::cout << "TypeOfIntegral(5l): " << TypeOfIntegral(5l) << '\n';
std::cout << "TypeOfIntegral(5L): " << TypeOfIntegral(5L) << '\n';
std::cout << "TypeOfIntegral(5ll): " << TypeOfIntegral(5ll) << '\n';
std::cout << "TypeOfIntegral(5LL): " << TypeOfIntegral(5LL) << '\n';

std::cout << '\n';

std::cout << "TypeOfIntegral(5ul): " << TypeOfIntegral(5ul) << '\n';
std::cout << "TypeOfIntegral(5.5): " << TypeOfIntegral(5.5) << '\n';

std::cout << '\n';

}

Im Programm overloadPattern.cpp besteht das Overload Set aus Lambda-Ausdrücken, die einen int, einen unsigned int, einen long int, einen long long int und auto akzeptieren. auto ist der Fallback, der verwendet wird, wenn das Overload Set mit einem unbekannten Typ aufgerufen wird. Das passiert zum Beispiel, wenn ich TypeOfIntegral mit einem unsigned long oder einem double Wert aufrufe.

Clevere Tricks mit Parameterpacks und Fold Expressions

Wie geht es weiter?

Normalerweise kommt das Overload Pattern für eine std::variant zum Einsatz, die eine typsichere Union ist. Eine Instanz var von std::variant (C++17) hat einen Wert aus einem ihrer Typen. std::visit ermöglicht es, einen Besucher auf var anzuwenden. Genau hier kommt das Overload Pattern sehr praktisch ins Spiel. Lies mehr über std::variant, std::visit und das Overload Pattern in meinem nächsten Beitrag.

Pdf-Päckchen: C++20 Module

Auf der Grundlage der letzten Umfrage habe ich das nächste Pdf-Päckchen erstellt.

Clevere Tricks mit Parameterpacks und Fold Expressions

Das pdf-Päckchen enthält alle

  • Beiträge.
  • Quellcode-Dateien zu diesen Beiträgen.

In dem Artikel "The New pdf Bundle is Ready: C++20 Modules" erkläre ich, wie man das pdf-Päckchen einfach erhalten kann.