C++ Core Guidelines: Besser spezifisch oder generisch?

Concepts werden die Art und Weise verändern, wie wir über generische Programmierung denken und sie anwenden. Sie haben es nicht in C++11 oder C++17 geschafft, werden aber mit hoher Wahrscheinlichkeit Bestandteil von C++20 sein.

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

Concepts werden die Art und Weise verändern, wie wir über generische Programmierung denken und sie anwenden. Sie haben es nicht in C++11 oder C++17 geschafft, werden aber mit hoher Wahrscheinlichkeit Bestandteil von C++20 sein.

Bevor ich über die Verwendung von Concepts schreibe, möchte ich gerne noch eine allgemeinere Bemerkung machen.

Bis C++20 kennen wir in C++ zwei diametrale Wege, Funktionen oder benutzerdefinierte Typen (Klassen) zu implementieren. Funktionen oder Klassen können für spezifische oder generische Datentypen implementiert werden. Im zweiten Fall nennen wir diese Funktions- oder Klassen-Templates. Was sind die Nachteile der beiden Varianten?

Es ist ein ganz schön aufwendiger Job, für jeden Datentyp eine Funktion oder eine Klasse zu definieren. Um diesen Job deutlich zu vereinfachen, besteht die Rettung aber auch ein großer Teil des Problems aus Typkonvertierungen.

Narrowing Conversion

Angenommen, du hast eine Funktion getInt(int a) definiert, die du mit einem double-Wert aufrufst. Nun tritt Narrowing Conversion in Aktion.

// narrowingConversion.cpp

#include <iostream>

void needInt(int i){
std::cout << "int: " << i << std::endl;
}

int main(){

std::cout << std::endl;

double d{1.234};
std::cout << "double: " << d << std::endl;

needInt(d);

std::cout << std::endl;

}

Ich denke nicht, dass das deine Absicht war. Mit einem double-Wert Wert hast du begonnen, mit einem int-Wert endest du.

Konvertierungen gibt es natürlich auch in die andere Richtung.

Integral Promotion

Angenommen, du hast eine Klasse MyHouse entworfen. Instanzen von MyHouse sollen in zwei Arten erzeugt werden können. Wenn der Konstruktor ohne Argument aufgerufen wird (1), wird das Attribut family auf einen leeren String gesetzt. Das bedeute, dass das Haus noch nicht bewohnt ist. Um einfach zu testen, ob das Haus bewohnt ist, soll der Konvertierungsoperator nach bool (2) zum Einsatz kommen. Gut! Oder?

// conversionOperator.cpp

#include <iostream>
#include <string>

struct MyHouse{
MyHouse() = default;
MyHouse(const std::string& fam): family(fam){}

operator bool(){ return !family.empty(); }

std::string family = "";
};

void needInt(int i){
std::cout << "int: " << i << std::endl;
}

int main(){

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

MyHouse firstHouse;
if (!firstHouse){
std::cout << "The firstHouse is still empty." << std::endl;
};

MyHouse secondHouse("grimm");
if (secondHouse){
std::cout << "Family grimm lives in secondHouse." << std::endl;
}

std::cout << std::endl;

needInt(firstHouse);
needInt(secondHouse);

std::cout << std::endl;

}

Nun lassen sich Instanzen von MyHouse verwenden, wenn ein int-Wert erforderlich ist. Seltsam!

Dank des überladenen Operators bool (2), können Instanzen von MyHouse als natürliche Zahlen verwendet werden. Damit sind sie in beliebigen arithmetischen Ausdrücken einsetzbar: auto res = MyHouse() + 5. Das war wohl nicht deine Absicht. Nur der Vollständigkeit halber: Seit C++11 kann der Konvertierungsoperator als explicit deklariert werden. Damit werden implizite Konvertierungen unterbunden.

Mein fester Glaube ist es, dass wir aus Gemütlichkeitsgründen die Magie der Konvertierung in C/C++ benötigen, da Funktionen nur spezifische Datentypen annehmen können.

Sind Templates die Rettung? Nein!

Generische Funktionen oder Klassen können mit beliebigen Werten aufgerufen werden. Falls die Werte die Anforderungen der Funktion oder Klasse nicht erfüllen, kein Problem. In diesem Fall erhältst du einen Fehlermeldung beim Kompilieren. Alles ist daher gut!

// gcd.cpp

#include <iostream>

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

int main(){

std::cout << std::endl;

std::cout << gcd(100, 10) << std::endl;

std::cout << gcd(3.5, 4.0)<< std::endl;
std::cout << gcd("100", "10") << std::endl;

std::cout << std::endl;

}

Was ist das Problem dieser Fehlermeldung?

Klar, die Fehlermeldung ist sehr wortreich und schwer verständlich. Das ist aber nicht mein zentraler Punkt. Die Kompilierung des Programms schlägt fehl, da weder doubles noch C-Strings den %-Operator unterstützen. Das heißt, der Fehler tritt durch eine fehlerhafte Instanziierung des Funktions-Templates für double-Werte und C-Strings auf. Dies ist deutlich zu spät. Es sollte keine Template-Instanziierung für die Werte double oder C-Strings möglich sein! Die Anforderungen für die Argumente sollten Bestandteil der Funktionsdeklaration sein und nicht zu einer fehlerhaften Instanziierung führen.

Jetzt kommen Concepts als Retter ins Spiel.

Mit Concepts betreten wir den Mittelweg. Mit ihnen lassen sich Funktionen oder Klassen definieren, die auf semantischen Kategorien agieren. Das heißt, dass die Argumente weder zu spezifisch noch zu generisch sind, sondern für Anforderungen mit Namen wie Integral stehen.

Sorry, der Artikel ist ein wenig kurz geraten, aber eine Woche vor meinem Mulithreading-Workshop auf der CppCon hatte ich weder die Zeit noch die Ressourcen dazu, (keine Verbindung in den Nationalparks des States Washington) einen vollständigen Artikel zu schreiben. Mein nächster Artikel wird besonders sein, denn ich werde über die CppCon schreiben. Danach geht es aber wieder um die generische Programmierung im Allgemeinen und Concepts im Besonderen. ()