iX 12/2017
S. 54
Titel
Programmiersprachen II
Aufmacherbild

Concepts in C++20: Mehr Details

The next big this

Der erste Artikel zu den für C++20 geplanten Concepts hat bereits gezeigt, wie man mit Klassen- und Funktions-Concepts C++-Code stärker abstrahieren kann. Auch ihre Syntax lässt sich noch weiter vereinfachen.

Die Geschichte zu Concepts in C++20 geht weiter. Dieser Artikel zeigt, wie sich die Syntax für Templates, Concepts und Platzhalter stärker vereinfachen lässt. Außerdem geht er der Frage nach, inwiefern man Haskells Typklassenhierarchie in C++ mit Concepts modellieren kann.

Um den roten Faden fortzuführen: Im ersten Artikel zu diesem Thema auf Seite 48 ist zu lesen, dass sich eingeschränkte Platzhalter (Concepts) überall dort einsetzen lassen, wo auch uneingeschränkte Platzhalter (auto) erlaubt sind.

Listing 1: Syntactic Sugar für Platzhalter

 1 #include <type_traits>
 2 #include <iostream>
 3
 4 template<typename T>
 5 concept bool Integral(){
 6   return std::is_integral<T>::value;
 7 }
 8 
 9 template<typename T>
10 requires Integral<T>()
11 T gcd(T a, T b){
12   if( b == 0 ){ return a; }
13   else{
14     return gcd(b, a % b);
15   }
16 }
17 
18 template<Integral T>
19 T gcd1(T a, T b){
20   if( b == 0 ){ return a; }
21   else{
22     return gcd(b, a % b);
23   }
24 }
25 
26 Integral gcd2(Integral a, Integral b){
27   if( b == 0 ){ return a; }
28   else{
29     return gcd(b, a % b);
30   }
31 }
32 
33 auto gcd3(auto a, auto b){
34   if( b == 0 ){ return a; }
35   else{
36     return gcd(b, a % b);
37   }
38 }
39 
40 int main(){
41 
42   std::cout << std::endl;
43 
44   std::cout << "gcd(100, 10)= "  <<gcd(100, 10)  << std::endl;
45   std::cout << "gcd1(100, 10)= " << gcd1(100, 10)  << std::endl;
46   std::cout << "gcd2(100, 10)= " << gcd2(100, 10)  << std::endl;
47   std::cout << "gcd3(100, 10)= " << gcd3(100, 10)  << std::endl;
48 
49   std::cout << std::endl;
50 
51 }

Hier soll es jetzt „süß“ weitergehen, nämlich mit Syntactic Sugar, syntaktischen Ausdrücken, die das Lesen einer Programmiersprache erleichtern (siehe dazu den Link unter ix.de/ix171054). Die folgenden Ausführungen zeigen, wie man Platzhalter einfacher anwenden kann. In bekannter Manier kommt in Listing 1 der GCD-Algorithmus (Greatest Common Divisor) zusammen mit dem Concept Integral zum Einsatz, um ihn typsicher zu machen.

Die Zeilen 4 bis 7 enthalten das aus dem ersten Artikel bekannte Concept Integral, das in den Zeilen 9 bis 31 mehrfach eingesetzt wird. Dabei wird die Syntax immer einfacher. Anstatt in Zeile 18 das Concept in der requires-Klausel (Zeile 10) zu verwenden, kommt es dort anstelle des gewohnten Schlüsselworts typename oder class für den Typparameter zum Einsatz. Es wird noch „süßer“. In Zeile 26 dient das Concept Integral als Funktionsparameter. Durch das Concept wird gcd2 zu einem Funktions-Template.

Weiter geht es mit der Vereinheitlichung. Falls ein uneingeschränkter Platzhalter (auto) in Zeile 33 den eingeschränkten Platzhalter (Integral) in Zeile 26 ersetzt, resultiert ein uneingeschränktes Funktions-Template. Der neue Ausdruck „uneingeschränktes Funktions-Template“ steht dafür, dass gcd3 ein Template ist, das Werte beliebigen Typs annehmen kann.

Syntaktische Variationen für Platzhalter: das Concept Integral in unterschiedlichen Varianten mit demselben Ergebnis (Abb. 1).

Abbildung 1 zeigt die Ausgabe des Programms. Man muss es mit mindestens einem GCC 6.3 und dem Flag fconcepts übersetzen.

Feinheiten und Überladen von Funktionen

Listing 2: Überladen von Funktionen mit konkreten Datentypen und Platzhaltern

 1 #include <iostream>
 2 #include <typeinfo>
 3 
 4 template<typename T>
 5 concept bool Integral(){
 6   return std::is_integral<T>::value;
 7 }
 8 
 9 void overload(auto t){
10   std::cout << "auto : " << t << ⤦
 std::endl;
11 }
12 
13 void overload(Integral t){
14   std::cout << "Integral : " << t << ⤦
 std::endl;
15 }
16 
17 void overload(long t){
18   std::cout << "long : " << t << ⤦
 std::endl;
19 }
20   
21 void twoTypes(auto a, auto b){
22   std::cout << typeid(a).name() << ⤦
 std::endl;
23   std::cout << typeid(b).name() << ⤦
 std::endl;
24 }
25 
26 
27 int main(){
28    
29   std::cout << std::endl;
30   
31   overload(3.14);
32   overload(2010);
33   overload(2020l);
34   
35   std::cout << std::endl;
36   
37   twoTypes(2010, 3.14); 
38   
39   std::cout << std::endl;
40 
41}

Das Funktions-Template gcd3 in Listing 1 ist mächtiger als das Funktions-Template gcd2. gcd3 besitzt zwei Typparameter, die verschieden sein dürfen. Diese Aussage gilt nicht für das Funktions-Template gcd2. Dort müssen a und b vom selben Typ sein, und den muss das Concept Integral selbstverständlich akzeptieren. Die Anwendung des Funktions-Templates twoTypes in Listing 2 macht diesen Punkt klar.

Das Funktions-Template twoTypes lässt sich mit konkreten Datentypen und Platzhaltern überladen (Abb. 2).

Mit ein wenig Hilfe von Run Time Type Information (RTTI) in den Zeilen 22 und 23 ermitteln beide Zeilen die String-Repräsentation der Variablen a und b. Wie erwartet zeigt die Ausgabe des Programms in Abbildung 2 die Ergebnisse int und double.

Die Abbildung zeigt auch schön das Zusammenspiel von konkreten Datentypen und Platzhaltern beim Überladen von Funktionen. In den Zeilen 31, 32 und 33 ruft Listing 2 die Funktion overload mit jeweils dem Datentyp double, int und long int auf. Der Compiler zieht den konkreten Datentyp long int (Zeile 17) dem eingeschränkten Platzhalter Integral (Zeile 13) vor. Letzteren zieht er wiederum dem uneingeschränkten Platzhalter auto (Zeile 9) vor. Die Auswahl der passenden Überladung vollzieht der Compiler daher gemäß seiner Strategie: vom Speziellen zum Allgemeinen. (Siehe dazu den Kasten „Teilweise Template-Spezialisierung“.)

Listing 3: Template Introduction

 1 #include <type_traits>
 2 #include <iostream>
 3 
 4 template<typename T>
 5 concept bool Integral(){
 6   return std::is_integral<T>::value;
 7 }
 8 
 9 Integral{T}
10 Integral gcd(T a, T b){
11   if( b == 0 ){ return a; }
12   else{
13     return gcd(b, a % b);
14   }
15 }
16 
17 Integral{T} 
18 class ConstrainedClass{};
19 
20 /*
21 
22 auto{T}
23 T gcd(T a, T b){
24   if( b == 0 ){ return a; }
25   else{
26     return gcd(b, a % b);
27   }
28 }
29 
30 auto{T} 
31 class ConstrainedClass{};
32 
33 */
34 
35 
36 int main(){
37   
38   std::cout << std::endl;
39   
40   auto res= gcd(100, 10); 
41 
42   ConstrainedClass<int> constrainedClass;
43   ConstrainedClass<double> ⤦
 constrainedClass1;
44   
45   std::cout << std::endl;
46 
47 }

Wer die klassische Art und Weise, Templates zu deklarieren, nicht mag, dem bietet C++20 eine „süßere“ Form an. Statt ein Template in der Form template<Integral T> zu deklarieren, lässt es sich mit „Template Introduction“ direkt durch Integral{T} ausdrücken. Diese vereinfachte Syntax mit geschweiften statt eckigen Klammern ist nur für eingeschränkte Platzhalter (Concepts) zulässig.

Neue Syntax durch Template Introduction