C++20: Concepts – Syntactic Sugar

Der Blogbeitrag beschäftigt sich mit Abbreviated Function Templates, die es auf "sehr süße" Art erlauben, Templates zu definieren.

In Pocket speichern vorlesen Druckansicht 152 Kommentare lesen
Lesezeit: 8 Min.
Von
  • Rainer Grimm
Inhaltsverzeichnis

In meinem Blogbeitrag geht es nicht um ein neues Feature von Concepts, sondern um Syntactic Sugar. Ich beschäftige mich heute mit Abbreviated Function Templates, die es auf "sehr süße" Art erlauben, Templates zu definieren.

So lautet Wikipedias Definition von Syntactic Sugar:

"In computer science, syntactic sugar is syntax within a programming language that is designed to make things easier to read or to express. It makes the language "sweeter" for human use: things can be expressed more clearly, more concisely, or in an alternative style that some may prefer."

In meinem letzten Artikel zu Concepts C++20: Concepts - die Placeholder Syntax ging ich darauf ein, dass wir seit C++14 eine starke Asymmetrie in C++ besitzen: Generische Lambda erlauben auf eine neue Art, Funktions-Templates zu definieren. Dazu muss lediglich auto als Parameter verwendet werden. Im Gegensatz dazu, lässt sich auto aber nicht als Parameter einer Funktion verwenden, um ein Funktions-Template zu erzeugen.

// genericLambdaFunction.cpp

#include <iostream>
#include <string>

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

auto addFunction(auto fir, auto sec){ return fir + sec; } // (2)

int main(){

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

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

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

std::cout << std::endl;

}

Der Clang-Compiler spricht in diesem Fall eine eindeutige Sprache.

Wie seltsam! Ich kann zwar auto als Rückgabetyp und als Funktionsparameter eines Lambda-Ausdruckes (Zeile 1) verwenden, bei einer Funktion kann ich es aber nur als Rückgabetyp einsetzen.

Nun kommt die gute Nachricht. Dieses inkonsistente Verhalten verschwindet mit C++20 und die Konsistenz geht mit Concepts weiter.

// conceptsIntegralVariationsDraft.cpp

#include <type_traits>
#include <iostream>

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

template<typename T> // (2)
requires Integral<T>
T gcd(T a, T b){
if( b == 0 ) return a;
else return gcd(b, a % b);
}

template<typename T> // (3)
T gcd1(T a, T b) requires Integral<T>{
if( b == 0 ){ return a; }
else return gcd(b, a % b);
}

template<Integral T> // (4)
T gcd2(T a, T b){
if( b == 0 ){ return a; }
else return gcd(b, a % b);
}

Integral auto gcd3(Integral auto a, Integral auto b){ // (5)
if( b == 0 ){ return a; }
else return gcd(b, a % b);
}

auto gcd4(auto a, auto b){ // (6)
if( b == 0 ){ return a; }
return gcd(b, a % b);
}

int main(){

std::cout << std::endl;

std::cout << "gcd(100, 10)= " << gcd(100, 10) << std::endl;
std::cout << "gcd1(100, 10)= " << gcd1(100, 10) << std::endl;
std::cout << "gcd2(100, 10)= " << gcd2(100, 10) << std::endl;
std::cout << "gcd3(100, 10)= " << gcd3(100, 10) << std::endl;
std::cout << "gcd4(100, 10)= " << gcd3(100, 10) << std::endl;

std::cout << std::endl;

}

Ich möchte zuerst kompakt die Fakten aus meinem vorherigen Artikel zu Concepts zusammenfassen. In der Zeile 1 definiere ich das Concept Integral. gcd - gcd2 (Zeilen 2 - 4) wenden das Concept in verschiedenen Arten an. gcd besitzt eine Requires Clause, gcd1 die sogenannte Trailing Requires Clause und gcd2 Constrained Template Parameters.

Mit gcd3 beginnt der Syntactic Sugar. Die Funktionsdeklaration Integral auto gcd3(Integral auto a, Integral auto b) fordert von seinen Typ-Parametern, dass diese das Concept Integral unterstützen. Aus der Anwendung des Concepts entsteht ein Funktions-Template, dass äquivalent zu den vorherigen Funktions-Templates gcd - gcd2 ist.

Die neue syntaktische Form von gcd3 und gcd4 heißt Abbreviated Function Templates. Integral auto in der Deklaration von gcd3 ist ein Constrained Placeholder (Concept). Es lässt sich aber auch ein Unconstrained Placeholder (auto) in der Funktionsdeklartion wie im Falle von gcd4 (Zeile 4) einsetzen. Bevor ich ein paar Anmerkungen zur neuen Syntax mache, ist hier zuerst die Ausgabe des Programms:

Durch die Verwendung eines Unconstrained Placeholders (auto) in der Funktionsdeklaration wird eine Funktions-Template erzeugt. Die zwei folgenden Funktionen sind äquivalent.

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

auto add(auto fir, auto sec){
return fir + sec;
}

Die entscheidende Beobachtung bei dem Beispiel ist es, dass die Argumente verschiedene Datentypen besitzen können. Das gleiche gilt für Concepts.

template<Arithmetic T, Arithmetic T2>                           // (1)
auto sub(T fir, T2 sec){
return fir - sec;
}

Arithmetic auto sub(Arithmetic auto fir, Arithmetic auto sec){ // (2)
return fir - sec;
}

Die Funktion sub fordert von ihren Argument fir und sec, dass beide das Concept Arithmetic unterstützen. Das heißt, du kannst sub(5.5, 5) aufrufen und dies ist für das Funktions-Template (Zeile 1) und die Funktion (Zeile 2) gültig. In beiden Fällen wird der Rückgabetyp gemäß den Regeln zur arithmetischen Konvertierung bestimmt. Das Concept Arithmetic fordert von fir und sec, dass diese entweder Ganzzahlen oder Fließkommazahlen sind. Hier ist eine naheliegende Definition mithilfe der Type-Traits Funktions std::is_arithmetic.

template<typename T> 
concept Arithmetic = std::is_arithmetic<T>::value;

Ich bin explizit darauf eingegangen, dass beide Argumente verschiedene Datentypen in dem Concepts Draft besitzen können, denn darin unterscheidet sich die ursprüngliche Concepts-Syntax, die von dem GCC umgesetzt wurde. Die ursprüngliche Syntax basierte auf dem Concepts TS (Technical Specification). In dieser Syntax mussten die Datentypen von fir und sec identisch sein, sodass ein Aufruf der Funktion sub(5.5, 5) zum Compilerfehler führte. Zusätzlich verwendete die ursprüngliche Syntax kein zusätzliches auto bei der Verwendung von Concepts und die Definition des Concepts war ein wenig wortreicher.

// conceptsArithmeticTS.cpp

#include <type_traits>
#include <iostream>

template<typename T>
concept bool Arithmetic(){
return std::is_arithmetic<T>::value;
}

Arithmetic sub(Arithmetic fir, Arithmetic sec){
return fir - sec;
}

int main(){

std::cout << std::endl;

std::cout << "sub(6, 5): " << sub(6, 5) << std::endl; // (1)
std::cout << "sub(5.5, 5): " << sub(5.5, 5) << std::endl; // (2)

std::cout << std::endl;

}

Aufgrund der Zeile 2 schlägt die Kompilierung des Programms mit dem Concepts TS fehl.

Die Abbreviated-Function-Templates-Syntax verhält sich vollkommen intuitiv. Sie unterstützt das Überladen von Funktionen. Wie gewohnt, wählt der Compiler die am Besten passende Funktion aus.

// conceptsOverloading.cpp

#include <type_traits>
#include <iostream>

template<typename T>
concept Integral = std::is_integral<T>::value;

void overload(auto t){
std::cout << "auto : " << t << std::endl;
}

void overload(Integral auto t){
std::cout << "Integral : " << t << std::endl;
}

void overload(long t){
std::cout << "long : " << t << std::endl;
}

int main(){

std::cout << std::endl;

overload(3.14); // (1)
overload(2010); // (2)
overload(2020l); // (3)

std::cout << std::endl;

}

Wenn die Funktion mit einem double (Zeile 1), einem int (Zeile 2) oder mit einem long int aufgerufen wird (Zeile 3), wählt der Compiler die am besten passende Funktion aus.

Zwei Komponenten der Conceps fehlen noch in meiner Miniserie zu Concepts: Concepts definieren und die sogenannte Template Introduction, die lediglich Bestandteil des Concepts TS ist. Beide Komponenten sind natürlich das Thema meines nächsten Artikels. ()