C++ Core Guidelines: Regeln für die Definition von Concepts

Modernes C++  –  0 Kommentare

Obwohl die Regel T.11 lautet: "Whenever possible use standard concepts", steht ab und zu die Aufgabe an, ein Concept zu definieren. Hierzu gibt es Regeln zu beachten.

Die C++ Core Guidelines besitzen neun Regeln für die Definition von Concepts. Sieben von ihnen besitzen einen Inhalt. Hier sind die ersten vier:

Los geht es mit der Semantik von Concepts:

Diese Regel ist ziemlich offensichtlich, doch was heißt bedeutungsvolle Semantik (meaningful semantic)? Sie besteht nicht nur aus einfachen Einschränkungen wie zum Beispiel has_plus, sondern auch aus Concepts wie Number, Range oder InputIterator.

Zum Beispiel fordert das Concept Addable lediglich die Unterstützung von has_plus und wird damit bereits durch einen String unterstützt:

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

string xx = "7";
string yy = "9";
auto zz = algo(xx, yy); // zz = "79"

Ich nehme an, dies war nicht deine Absicht, denn das Funktions-Template algo sollte nur Zahlen annehmen. Es soll aber nicht Objekte akzeptieren, die nur addierbar sind. Die Lösung ist einfach: Definiere ein Concept Number, das eine bedeutungsvolle Semantik besitzt:

template<typename T>
// The operators +, -, *, and / for a number
// are assumed to follow the usual mathematical rules
concept Number = has_plus<T>
&& has_minus<T>
&& has_multiply<T>
&& has_divide<T>;

template<Number N> auto algo(const N& a, const N& b)
{
// ...
return a + b;
}

Nun gibt der Funktionsaufruf von algo mit einem String einen Fehler aus. Die nächste Regel ist ein Spezialfall dieser Regel.

Was ist eine vollständige Menge (complete set) eines Concepts? Die Guidelines stellen zwei Beispiele vor: Arithmetic und Comparable:

  • Arithmetic: +, -, *, /, +=, -=, *=, /=
  • Comparable: <, >, <=, >=, ==, !=

Für was steht das Akronym POLA? Für Principle Of Least Astonishment. Dieses Prinzip guten Softwareentwurfs lässt sich einfach brechen, indem ein Concepts nur teilweise implementiert wird. Hier ist ein Beispiel der Guidelines; das Concept Minimal unterstützt in diesem Fall ==, und +:

void f(const Minimal& x, const Minimal& y)
{
if (!(x == y)) { /* ... */ } // OK
if (x != y) { /* ... */ } // surprise! error

while (!(x < y)) { /* ... */ } // OK
while (x >= y) { /* ... */ } // surprise! error

x = x + y; // OK
x += y; // surprise! error
}

Was ist ein Axiom? Hier die Definition aus Wikipedia:

An axiom or postulate is a statement that is taken to be true, to serve as a premise or starting point for further reasoning and arguments.

Da C++ keine Axiome unterstützt, lassen sie sich nur durch Kommentare ausdrücken. Falls C++ Axiome in der Zukunft unterstützen wird, ist es im Wesentlichen ausreichend, das Kommentarsymbol // aus dem folgenden Beispiel zu entfernen:

template<typename T>
// axiom(T a, T b) { a + b == b + a; a - a == 0; a * (b + c) == a * b + a * c; /*...*/ }
concept Number = requires(T a, T b) {
{a + b} -> T;
{a - b} -> T;
{a * b} -> T;
{a / b} -> T;
}

Das Axiom bedeutet in dem Fall, dass Number den mathematischen Regeln folgen soll. Im Gegensatz dazu fordert das Concept, dass Number die binären Operationen +, -, * und / unterstützen muss und das Ergebnis nach T konvertiert ist. T ist der Typ der Argumente.

Falls zwei Concepts dieselben Anforderungen stellen, sind sie logisch äquivalent. Dies heißt, dass der Compiler diese nicht unterscheiden kann und daher nicht automatisch die richtige Variante wählt, wenn Funktionen überladen werden. Um diese Regel anschaulich zu beschreiben, habe ich vereinfachte Versionen der Concepts BidirectionalIterator und RandomAccessIterator implementiert:

template<typename I>
concept bool BidirectionalIterator =
ForwardIterator<I> &&
requires(I iter){
--iter;
iter--;
}

template<typename I>
concept bool RandomAccessIterator =
BidirectionalIterator<I> &&
Integer<N> &&
requires(I iter, I iter2, N n ){
iter += n; // increment or decrement an iterator
iter -= n;
n + iter; // return a temp iterator
iter + n;
iter - n;
iter[n]; // access the element
iter1 - iter2; // subtract two iterators
iter1 < iter2; // compare two iterators
iter1 <= iter2;
iter1 > iter2;
iter1 >= iter2;
}

std::advance(i, n) inkrementiert einen Iterator i um n Elemente. Wenn der Iterator i ein bidirektionaler Iterator ist, muss std::advance elementweise n Schritte vorwärts oder rückwärts gehen. Wenn der Iterator i im Random Access Iterator ist, wird lediglich einmalig n auf den Iterator i addiert:

template<BidirectionalIterator I>
void advance(I& iter, int n){...}

template<RandomAccessIterator I>
void advance(I& iter, int n){...}

std::list<int> lst{1, 2, 3, 4, 5, 6, 7, 8, 9};
std::list<int>::iterator listIt = lst.begin();
std::advance(listIt, 2); // BidirectionalIterator

std::vector<int> vec{1, 2, 3, 4, 5, 6, 7, 8, 9};
std::vector<int>::iterator vecIt = vec.begin();
std::advance(vecIt, 2); // RandomAccessIterator

Im Falle eines std::vector<int>, gibt vec.begin() einen Random Access Iterator zurück. Somit wird die schnellere Variante von std::advance verwendet.

Jeder Container der Standard Template Library erzeugt einen Iterator, der seine Struktur widerspiegelt. Hier ist ein Überblick zu den Containern und ihren Iteratoren:

Drei Reglen für die Defintion von Concepts sind noch übrig. Insbesondere die nächste Regel "T.24: Use tag classes or traits to differentiate concepts that differ only in semantics." klingt sehr interessant. Im nächsten Artikel schaue ich mir an, was Tag Classes oder Traits Classes sind.