C++ Core Guidelines: Regeln zu Templates und generischer Programmierung

Modernes C++  –  7 Kommentare

In diesem Artikel führe ich die Regeln zur generischen Programmierung in C++ ein. Generische Programmierung ist meines Erachtens das herausragende Feature und die Zukunft von C++. Daher geht es heute und in den weiteren Artikeln um die Zukunft von C++.

Ich unterscheide nicht scharf zwischen Templates und generischer Programmierung. Einer der beiden Begriffe kommt in dem Artikel zum Einsatz, wenn er am besten passt. Natürlich weiß ich, dass Templates nur eine Technik sind, generischen Code zu schreiben. Ich nehme an, dass dir der Begriff Templates in C++ vertraut ist. Dies gilt aber wohl nicht für generische Programmierung. Hier ist die Definition aus Wikipedia:

Generic programming is a style of computer programming in which algorithms are written in terms of types to-be-specified-later that are then instantiated when needed for specific types provided as parameters.

Die Regeln zur generischen Programmierung behandeln den aktuellen C++17- und anstehenden C++20-Standard. Ich gehe dabei davon aus, dass wir mit C++20 Concepts erhalten werden. In Summe enthalten die C++ Core Guidelines 100 Regeln zu Concepts, Template Interfaces, Template-Definitionen, Template-Hierarchien, Variadic Templates und Template-Metaprogrammierung. Die ersten fünf Regeln sind allgemeiner Natur:

In den Beispielen sind Concepts meist auskommentiert. Wenn du sie ausprobierten willst, entferne die Kommentare und verwende zumindest den GCC 6.1 Compiler mit dem Flag -fconcepts oder ein Online-Compiler: constraints and concepts.

Concepts sind Prädikate für Templates, die zu Compilezeit ausgewertet werden. Sie sollen semantische Kategorien wie Number, Callable, Iterator oder Range beschreiben, aber nicht für syntaktische Einschränkungen wie HasPlus oder IsInvocable stehen. Hier gibt es mehr Details zu Concepts.

Eventuell verwirren dich die zwei Begriffe semantische Kategorien und syntaktische Einschränkungen. Die erste Regel sorgt für Aufklärung.

T.1: Use templates to raise the level of abstraction of code

Hier kommt das Beispiel aus den Guidelines. Das zweite Concept habe ich Addable genannt:

template<typename T>
// requires Incrementable<T>
T sum1(vector<T>& v, T s)
{
for (auto x : v) s += x;
return s;
}

template<typename T>
// requires Addable<T>
T sum2(vector<T>& v, T s)
{
for (auto x : v) s = s + x;
return s;
}

Was stimmt bei den beiden Concepts nicht? Beide sind viel zu spezifisch. Beide Concepts basieren auf konkreten Operationen wie Inkrementieren und Addieren. Da will ich gerne ein Schritt weiter von den syntaktischen Einschränkungen zu der semantischen Kategorie Arithmetic gehen:

template<typename T>
// requires Arithmetic<T>
T sum(const vector<T>& v, T s)
{
for (auto x : v) s += x;
return s;
}

Nun besitzt der Algorithmus die minimalen Anforderungen. Stopp! Der Algorithmus ist besser, aber nicht gut. Er setzt einen std::vector als Container voraus. Es ist zwar generisch bezüglich der Elemente seines Containers, aber nicht auf den Containern selbst. Daher lässt sich der Algorithmus noch allgemeiner formulieren:

template<typename Cont, typename T>
// requires Container<Cont>
// && Arithmetic<T>
T sum(const Cont& v, T s)
{
for (auto x : v) s += x;
return s;
}

Nun besitzt der Algorithmus die richtige Abstraktion. Vielleicht ziehst du ja eine kompaktere Schreibweise vor. Anstelle des Schlüsselworts typename wende ich das Concept jetzt direkt an:

template<Container Cont, Arithmetic T>
T sum(const Cont& cont, T s){
for (auto x : cont) s += x;
return s;
}

T.2: Use templates to express algorithms that apply to many argument types

Die erst Überladung von std::find besitzt laut cppreferen.com die folgende Deklaration:

template< class InputIt, class T >
InputIt find( InputIt first, InputIt last, const T& value );

Die Typen der Iteratoren sind in ihren Namen kodiert: InputIt steht für einen Input Iterator und bedeutet, dass dieser Iterator das Objekt lesen kann, auf das er verweist. Die Deklaration besitzt zwei Schwächen.

  1. Die Anforderungen an den Iterator sind im Namen kodiert. Das erinnert mich an die heftig in meinen Schulungen diskutierte ungarische Notation.
  2. Es gibt keine Anforderungen, die sicherstellt, dass das vom Iterator referenzierte Objekt mit dem value verglichen werden kann.

Daher will ich die Concepts direkt verwenden:

template<Input_iterator Iter, typename Val>
// Equality_comparable<Value_type<Iter>, Val>
Iter find(Iter b, Iter e, Val v)
{
// ...
}

T.3: Use templates to express containers and ranges

Es ist naheliegend, einen Container generisch anzulegen. Hier ist zum Beispiel ein Vector:

template<typename T>
// requires Regular<T>
class Vector -{
// ...
T* elem; // points to sz Ts
int sz;
};

Vector<double> v(10);
v[7] = 9.9;

Ein Frage bleibt bei dem Beispiel noch offen. Wann ist ein benutzerdefinierter Datentyp regulär? Das Dokument "Fundamentals of Generic Programming" sagt aus, wenn sich der Datentyp wie ein Built-in-Datentyp (z.B.: bool, int oder double) erhält. Ich muss natürlich erwähnen, dass das Dokument aus den Federn von James C. Dehnert und Alexander Stepanov stammt. Vermutlich weißt du, wer Stepanov ist. Er ist der bekannte Vater der Standard Template Library (STL).

Laut dem Dokument ist ein Datentyp regulär, wenn er die folgenden Operationen anbietet:

Die Gleich- und Ungleichheitsoperatoren können wie die Ordnungsoperatoren auf der Ebene der Komponenten definiert werden.

Wie geht's weiter?

Mein ursprünglicher Plan war es, über die Regel 5.5: Combine generic and OO techniques to amplify their strengths zu schreiben. Ich habe aber meinen Plan geändert, da die Regel zum einen sehr kurz ist und zum anderen Type Erasure als Anwendungsfall vorstellt. Type Erasure ist eine Technik, die es erlaubt, auf verschiedene Datentypen ein generisches Interface anzubieten. Diese anspruchsvolle Technik lässt sich aber nicht in ein paar Sätzen erklären. Dazu benötige ich den ganzen nächsten Artikel.