C++20: Concepts – die Placeholder Syntax

Modernes C++  –  85 Kommentare

Heute gebe ich eine einfache Antwort auf eine herausfordernde Frage: Wann lassen sich Concepts einsetzen? Sie lassen sich an den Stellen einsetzen, an den auto verwendet werden kann.

Bevor ich über die Placeholder-Syntax und die neue Syntax für Funktions-Templates schreibe, möchte ich ein wenig ausholen. In C++11/14 gibt es einige Asymmetrien.

Von Asymmetrien in C++11/14 zu Symmetrien in C++20

Ich habe öfters in meinem Seminar Diskussionen der folgenden Art:

Was ist die Standard Template Library aus der Vogelperspektive? Generische Container die sich mit generischen Algorithmen manipulieren lassen. Das Bindeglied zwischen den beiden disjunkten Komponenten sind Iteratoren.

Generische Container und generische Algorithmen bedeuten, dass sie nicht an einen speziellen Datentyp gebunden sind. Viele der Algorithmen lassen sich durch eine aufrufbare Einheit parametrisieren. So besitzt zum Beispiel std::sort eine Überladung, die ein binäres Prädikat (aufrufbare Einheit) annimmt. Das binäre Prädikat wird dabei auf die Elemente des Containers angewandt. Ein binäres Prädikat ist eine Funktion, die zwei Parameter besitzt und einen Wahrheitswert zurückgibt. Du kannst dafür eine Funktion, ein Funktionsobjekt oder mit C++11 einen Lambda-Ausdruck als binäres Prädikat verwenden.

Eine leicht Asymmetrie in C++11

Welchen konzeptionellen Fehler besitzt das folgende Programm? (Ich weiß, dass wir ein vordefiniertes Prädikat std::greater in C++ besitzen.)

// lambdaCpp11.cpp

#include <algorithm>
#include <iostream>
#include <string>
#include <array>
#include <vector>

template <typename Cont, typename Pred>
void sortDescending(Cont& cont, Pred pred){
std::sort(cont.begin(), cont.end(), pred);
}

template <typename Cont>
void printMe(const Cont& cont){
for (auto c: cont) std::cout << c << " ";
std::cout << std::endl;
}

int main(){

std:: cout << std::endl;

std::array<int, 10> myArray{5, -10, 3, 2, 7, 8, 9, -4, 3, 4};
std::vector<double> myVector{5.1, -10.5, 3.1, 2.0, 7.2, 8.3};
std::vector<std::string> myVector2{"Only", "for", "testing", "purpose"};

sortDescending(myArray,
[](int fir, int sec){ return fir > sec; }); // (1)
sortDescending(myVector,
[](double fir, double sec){ return fir > sec; }); // (2)
sortDescending(myVector2,
[](const std::string& fir, const std::string& sec){ // (3)
return fir > sec;
});

printMe(myArray);
printMe(myVector);
printMe(myVector2);

std::cout << std::endl;

}

Das Program besitzt ein std::array von ints, einen std::vector von doubles und einen std::vector von std::strings. Alle Container sollen in absteigender Ordnung sortiert und ausgegeben werden. Um mir meinen Job möglichst einfach zu machen, verwendete ich die zwei Funktions-Templates sortDescending und printMe.

Obwohl Conainter und Algorithmen generisch sind, besitzt C++11 lediglich typgebundene Lambdas. Daher muss ich ein binäres Prädikat für jeden Datentyp implementieren (Zeile 1 -3) und breche daher meinen generischen Weg. Mit C++14 verschwindet diese Asymmetrie. Container und Algorithmen sind generisch und Lambdas lassen sich generisch anwenden.

Die starke Asymmetrie in C++20

Mit C++14 wird die neue Implementierung des vorherigen Programms lambdaCpp11.cpp deutlich einfacher.

// lambdaCpp14.cpp

#include <algorithm>
#include <iostream>
#include <string>
#include <array>
#include <vector>

template <typename Cont>
void sortDescending(Cont& cont){
std::sort(cont.begin(), cont.end(), [](auto fir, auto sec){ // (1)
return fir > sec;
});
}

template <typename Cont>
void printMe(const Cont& cont){
for (auto c: cont) std::cout << c << " ";
std::cout << std::endl;
}

int main(){

std:: cout << std::endl;

std::array<int, 10> myArray{5, -10, 3, 2, 7, 8, 9, -4, 3, 4};
std::vector<double> myVector{5.1, -10.5, 3.1, 2.0, 7.2, 8.3};
std::vector<std::string> myVector2{"Only", "for", "testing", "purpose"};

sortDescending(myArray); // (2)
sortDescending(myVector); // (2)
sortDescending(myVector2); // (2)

printMe(myArray);
printMe(myVector);
printMe(myVector2);

std::cout << std::endl;

}

Alles gut? Ja und nein. Ja, da ich einen generischen Lambda-Ausdruck (Zeile 1) für jeden Datentyp in Zeile 2 verwenden kann. Nein, da ich die leichte Asymmetrie in C++11 mit einer starken Asymmetrie in C++14 ersetzt habe. Die leicht Asymmetrie in C++11 war, dass der Lambda-Ausdruck typgebunden war. Die starke Asymmetrie in C++14 ist es, dass generische Lambdas eine neue syntaktische Form definieren, um generische Funktion (Funktions-Templates) zu schreiben. Hier ist der Beweis.

// genericLambdaTemplate.cpp

#include <iostream>
#include <string>

auto addLambda = [](auto fir, auto sec){ return fir + sec; }; // (1)

template <typename T, typename T2> // (2)
auto addTemplate(T fir, T2 sec){ return fir + sec; }

int main(){

std::cout << std::boolalpha << std::endl;

std::cout << addLambda(1, 5) << " " << addTemplate(1, 5) << std::endl;
std::cout << addLambda(true, 5) << " " << addTemplate(true, 5)
<< std::endl;
std::cout << addLambda(1, 5.5) << " " << addTemplate(1, 5.5)
<< std::endl;

const std::string fir{"ge"};
const std::string sec{"neric"};
std::cout << addLambda(fir, sec) << " " << addTemplate(fir, sec)
<< std::endl;

std::cout << std::endl;

}

Der generische Lambda-Ausdruck in Zeile 1 produziert die gleichen Ergebnisse wie das Funktions-Template in Zeile 2.

Dies ist genau die Asymmetrie in C++14. Generische Lambdas führen eine neue Art Funktions-Template zu definieren ein.

Wenn ich dies in meinen Seminaren erkläre, bekomme ich fast immer die Frage: Können wir auto in Funktionsdeklaration verwenden um Funktions-Templates zu erhalten? Nein, nicht mit C++17, aber mit C++20. In C++20 können Constrained Placeholders (Concepts) oder Unconstrained Placeholder (auto) in Funktionsdeklaration verwendet werden. Damit wird aus der Funktion ein Funktions-Template. Das heißt natürlich auch, dass die Asymmetrie in C++20 überwunden ist.

Die Lösung in C++20

Bevor ich mich mit der neuen Art Funktions-Templates zu definieren beschäftigen will, möchte ich mich ursprüngliche Frage beantworten: Wann lassen sich Concepts einsetzen? Concepts lassen sich an den Stellen einsetzen, an den auto verwendet werden kann.

// placeholdersDraft.cpp

#include <iostream>
#include <type_traits>
#include <vector>

template<typename T> // (1)
concept Integral = std::is_integral<T>::value;

Integral auto getIntegral(int val){ // (2)
return val;
}

int main(){

std::cout << std::boolalpha << std::endl;

std::vector<int> vec{1, 2, 3, 4, 5};
for (Integral auto i: vec) std::cout << i << " "; // (3)

Integral auto b = true; // (4)
std::cout << b << std::endl;

Integral auto integ = getIntegral(10); // (5)
std::cout << integ << std::endl;

auto integ1 = getIntegral(10); // (6)
std::cout << integ1 << std::endl;

std::cout << std::endl;

}

Das Concept Integral in Zeile 1 lässt sich als Rückgabetyp (Zeile 2), in einer Range-base for-Schleife (Zeile 3), oder als Datentyp für die Variable b (Zeile 3) oder die Variable integ (Zeile 5) verwenden. Um die Symmetrie auf den Punkt zu bringen, wende ich in Zeile 6 Typdeduktion mit auto an. Ich muss zugeben, dass es zum jetzigen Zeitpunkt keinen Compiler gibt, der die für die Standardisierung vorgeschlagene Syntax, die ich in meinem Beispiel verwendete, vollständig unterstützt. Die folgende Ausgabe geht daher auf den GCC und die vorläufige Syntax des Concepts TS (Technical Specification) zurück.

Wie geht's weiter?

Mein nächster Artikel beschäftigt sich mit dem Syntactic Sugar, den wir mit Constrained Placeholders (Concepts) und Unconstrained Placeholders (auto) in C++20 erhalten. Ich denke, die Funktion (das Funktions-Template) getIntegral gibt einen ersten Vorgeschmack.