Concepts in C++20: Eine Evolution oder eine Revolution?
Stellen Concepts eine Evolution oder eine Revolution in C+++ dar? Eine Antwort auf diese philosophisch angehauchte Frage.
- Rainer Grimm
Heute schlieĂźe ich meine Miniserie zu Concepts mit der Antwort zur philosophisch angehauchten Frage ab: Stellen Concepts eine Evolution oder eine Revolution in C+++ dar?
Wir alle ahnen es, fĂĽr welche Begrifflichkeit Evolution und Revolution steht. Gerne will ich aber ein wenig genauer sein. Die Definitionen von re:invention sind kurz und bĂĽndig:
- Evolution is defined as gradual change, adaptation, progression, metamorphosis.
- Revolution is defined as forcible overthrow for an entirely new system…drastic, disruptive, far-reaching, momentous change.
Um es zu vereinfachen: Der entscheidende Unterschied zwischen einer Evolution und einer Revolution ist, ob die Veränderungen fließend (Evolution) oder sprunghaft (Revolution) erfolgen.
In meinen vorherigen Artikel gab es viele Diskussionen zu Concepts. Daher war ich auf eure Meinung zu meiner gestellten Frage sehr neugierig. Interessanterweise hatten die Antwort eine starke Tendenz zur Evolution. Ich jedoch neige mehr zur Revolution.
Welche Argumente sprechen nun fĂĽr die Evolution beziehungsweise die Revolution?
Evolution
Saubere Abstraktion
Vernünftig eingesetzt sollten Concepts das saubere Arbeiten mit generischem Code auf einer höheren Abstraktionsebene befördern. Auf längere Sicht könnte ich mir auch vorstellen, dass gerade die Standard-Concepts zunehmend idiomatisch werden sollten und dass damit auch die Interoperabilität und das modulare Arbeiten vor allem in größeren Teams robuster und weniger fehleranfällig gemacht werden kann, wenn mehr auf abstrakte Eigenschaften der Parameter-Klassen geprüft wird und weniger auf lediglich rein syntaktische "Ausrollbarkeit" in generischem Code.
Einfache Definition und sinnvolle Fehlermeldungen
Concepts können nichts, was man bisher – wenn auch gegebenenfalls sehr umständlich und aufwendig – nicht mit type-traits, SFINAE und static_assert
hinbekommen hätte. Ihr Vorteil liegt in der einfachen Definition und sinnvollen Fehlermeldungen.
Unconstrained Placeholders
Seit C++11 können wir mithilfe von auto
den Datentyp einer Variable von seinem Initialisierer ableiten:
auto integ = add(2000, 11);
std::vector<int> myVec{1, 2, 3};
for (auto v: myVec) std::cout << v << std::endl;
auto
ist eine Art uneingeschränkter Platzhalter. Mit C++20 ist diese Ableitung des Datentyps auch mit eingeschränkten Platzhaltern (Concepts) möglich:
template<typename T>
concept Integral = std::is_integral<T>::value;
Integral auto integ = add(2000, 11);
std::vector<int> myVec{1, 2, 3};
for (Integral auto v: myVec) std::cout << v << std::endl;
Um prägnant und evolutionär zu argumentieren: Eingeschränkte Platzhalter (Concepts) können überall dort verwendet werden, wo uneingeschränkte Platzhalter (auto
) verwendbar sind.
Generische Lambdas
Seit C++14 lassen sich generische Lambdas (addLambda
) einsetzen. Diese sind unter der Decke Funktions-Templates (addTemplate
):
// addLambdaGeneric.pp
#include <iostream>
auto addLambda = [](auto fir, auto sec){ return fir + sec; };
template <typename T, typename T2>
auto addTemplate(T fir, T2 sec){ return fir + sec; }
int main(){
std::cout << addLambda(2000, 11.5); // 2011.5
std::cout << addTemplate(2000, 11.5); // 2011.5
}
Die Verwendung von auto
in einer Funktionsdeklaration war in C++14 nicht möglich. Seit C++20 kannst du eingeschränkte (Concepts) und uneingeschränkte Platzhalter (auto
) in der Funktionsdeklaration verwenden. Intern wird die Funktionsdeklaration zu einem Funktions-Template mit eingeschränkten (Concept) oder uneingeschränkten (auto
) Platzhaltern:
// addUnconstrainedConstrained.cpp
#include <concepts>
#include <iostream>
auto addUnconstrained(auto fir, auto sec){
return fir + sec;
}
std::floating_point auto addConstrained(std::integral auto fir,
std::floating_point auto sec){
return fir + sec;
}
int main(){
std::cout << addUnconstrained(2000, 11.5); // 2011.5
std::cout << addConstrained(2000, 11.5); // 2011.5
}
Um meine Argumentation besser auf den Punkt zu bringen, besitzt die Funktion addConstrained
eine sehr diskussionswĂĽrdige Signatur.
Revolution
Template-Anforderungen prĂĽfen
Zugegeben, Anforderungen an Templates lassen sich in C++11 – halbwegs elegant – bereits in der Template-Deklaration prüfen:
// requirementsCheckSFINAE.cpp
#include <type_traits>
template<typename T,
typename std::enable_if<std::is_integral<T>::value, T>:: type = 0>
T moduloOf(T t) {
return t % 5;
}
int main() {
auto res = moduloOf(5.5);
}
Das Funktions-Template moduloOf
fordert, dass T
integral sein soll. Falls T
nicht integral
ist und daher der Ausdruck std::integral<T>::value false
ergibt, ist die fehlerhafte Substitution kein Fehler. Der Compiler entfernt diese konkrete Überladung aus der Menge aller möglichen Überladungen der Funktion moduloOf
. Leider gibt es danach keine gĂĽltige Ăśberladung mehr.
Diese Technik ist unter dem Name SFINAE bekannt. Das steht fĂĽr "Substitution Failure Is Not An Error". Auf diese Technik gehe ich nur in fortgeschrittenen Schulungen zu Templates ein. Das gilt aber nicht fĂĽr Concepts. Diese drĂĽcken ihre Intention direkt aus:
// requirementsCheckConcepts.cpp
#include <concepts>
std::integral auto moduloOf(std::integral auto t) {
return t % 5;
}
int main() {
auto res = moduloOf(5.5);
}
Die Definition von Templates ist deutlich einfacher
Dank der Abbreviated Funktion-Template Syntax wird die Definition von Templates in C++20 zum Kinderspiel. Ich habe bereits den neuen Syntactic Sugar in der Funktionsdeklaration von addConstrained
und mudolOf
vorgestellt. Daher lasse ich das Beispiel in diesem Abschnitt aus.
Semantische Kategorien
Concepts stehen nicht für syntaktische Einschränkungen, sondern für semantische Kategorien. Addable
ist ein Concept, das eine syntaktische Einschränkung repräsentiert.
template<typename T>
concept Addable = has_plus<T>; // bad; insufficient
template<Addable N> auto algo(const N& a, const N& b) // use two numbers
{
// ...
return a + b;
}
int x = 7;
int y = 9;
auto z = algo(x, y); // z = 16
std::string xx = "7";
std::string yy = "9";
auto zz = algo(xx, yy); // zz = "79"
Addable
verhält sich nicht erwartungsgemäß. Das Funktions-Template algo
sollte Argumente annehmen können, die eine Zahl modellieren und nicht lediglich den +-Operator unterstützen. Konsequenterweise lassen sich zwei Strings als Argument verwenden. Dies ist sehr fragwürdig, da die Addition kommutativ sein sollte. String-Konkatenation ist es aber nicht:
"7" + "9" != "9" + "7"
.
Die Lösung liegt auf der Hand. Definiere das Concept Number
. Es ist eine semantische Kategorie wie Equal
, Callable
, Predicate
oder Monade
.
Meine Antwort
Natürlich lassen sich gewichtige Argumente für die evolutionären Schritte oder einen revolutionären Sprung mit Concepts finden. Dank der semantischen Kategorien tendiere ich deutlich zur Revolution. Concepts wie Number, Equal oder Ordering erinnern mich an die platonische Welt der Ideen. Für mich ist es revolutionär, dass wir dank Concepts unsere Programme in diesen Kategorien analysieren können.
Wie geht's weiter
Die Ranges-Bibliothek, die ich in meinem nächsten Artikel genauer vorstelle, ist der erste Konsument der Concepts. Ranges unterstützen Algorithmen, die
- auf dem ganzen Container arbeiten.
- lazy evaluiert werden.
- komponiert werden können.