C++ Core Guidelines: Definition von Concepts, die Zweite

Modernes C++  –  0 Kommentare

Weiter geht es mit der Definition von Concepts. In diesem Artikel ist vor allem die erste der drei verbleibenden Regeln recht anspruchsvoll.

Dies sind die Regeln für den heutigen Artikel:

Die Erläuterungen zur ersten Regel sind ziemlich kompakt, zu kompakt.

T.24: Use tag classes or traits to differentiate concepts that differ only in semantics

Dies ist die Begründung der Regel aus den Guidelines: "Two concepts requiring the same syntax but having different semantics leads to ambiguity unless the programmer differentiates them."

Angenommen, ich habe das Trait is_contiguous definiert. In diesem Fall kann ich es verwenden, um einen Random Access Iterator RA_iter von einem Contiguous Iterator Contiguous_iter zu unterscheiden:

template<typename I> // iterator providing random access
concept bool RA_iter = ...;

template<typename I> // iterator providing random access to contiguous data
concept bool Contiguous_iter =
RA_iter<I> && is_contiguous<I>::value; // using is_contiguous trait

Ich kann weiter die Tag-Klasse is_contiguous in einem Concept verpacken und anwenden. Jetzt besitze ich einen leicht lesbaren Ausdruck, der meine Idee eines Contiguous Iterators Contiguous_iter umsetzt:

template<typename I> concept Contiguous = is_contiguous<I>::value;

template<typename I>
concept bool Contiguous_iter = RA_iter<I> && Contiguous<I>;

Okay, ich denke, ich sollte die zwei entscheidenden Begriffe Traits und Tag Dispatching erklären.

Traits

Traits sind Klassen-Templates, die Eigenschaften von generischen Datentypen ermitteln.

Das folgende Programm zeigt für jedes der 14 primären Typkategorien der Type-Traits-Bibliothek einen Typ, der das spezifische Trait erfüllt. Die primären Typkategorien sind vollständig und disjunkt. Daher ist jeder Datentyp genau Mitglied einer Typkategorie. Wenn du eine Typkategorie für einen Typ prüfst, wird dies unabhängig von const oder volatile Qualifiers durchgeführt:

// traitsPrimary.cpp

#include <iostream>
#include <type_traits>

using namespace std;

template <typename T>
void getPrimaryTypeCategory(){

cout << boolalpha << endl;

cout << "is_void<T>::value: " << is_void<T>::value << endl;
cout << "is_integral<T>::value: " << is_integral<T>::value << endl;
cout << "is_floating_point<T>::value: " << is_floating_point<T>::value << endl;
cout << "is_array<T>::value: " << is_array<T>::value << endl;
cout << "is_pointer<T>::value: " << is_pointer<T>::value << endl;
cout << "is_null_pointer<T>::value: " << is_null_pointer<T>::value << endl;
cout << "is_member_object_pointer<T>::value: " << is_member_object_pointer<T>::value << endl;
cout << "is_member_function_pointer<T>::value: " << is_member_function_pointer<T>::value << endl;
cout << "is_enum<T>::value: " << is_enum<T>::value << endl;
cout << "is_union<T>::value: " << is_union<T>::value << endl;
cout << "is_class<T>::value: " << is_class<T>::value << endl;
cout << "is_function<T>::value: " << is_function<T>::value << endl;
cout << "is_lvalue_reference<T>::value: " << is_lvalue_reference<T>::value << endl;
cout << "is_rvalue_reference<T>::value: " << is_rvalue_reference<T>::value << endl;

cout << endl;

}

int main(){

getPrimaryTypeCategory<void>(); // (1)
getPrimaryTypeCategory<short>(); // (1)
getPrimaryTypeCategory<double>();
getPrimaryTypeCategory<int []>();
getPrimaryTypeCategory<int*>();
getPrimaryTypeCategory<std::nullptr_t>();
struct A{
int a;
int f(double){return 2011;}
};
getPrimaryTypeCategory<int A::*>();
getPrimaryTypeCategory<int (A::*)(double)>();
enum E{
e= 1,
};
getPrimaryTypeCategory<E>();
union U{
int u;
};
getPrimaryTypeCategory<U>();
getPrimaryTypeCategory<string>();
getPrimaryTypeCategory<int * (double)>();
getPrimaryTypeCategory<int&>(); // (2)
getPrimaryTypeCategory<int&&>(); // (2)

}

Um dich nicht zu Tode zu langweilen, habe ich nur die Ausgabe der Zeilen (1)

und die Ausgaben der Zeilen (2) dargestellt:

Tag Dispatching

Tag Dispatching ermöglicht es, eine Funktion basierend auf den Eigenschaften ihres Typs auszuwählen. Die Entscheidung findet zur Compilezeit statt, und Traits, die ich im letzten Paragraph eingeführt habe, kommen dazu zum Einsatz.

Ein typisches Beispiel für Tag Dispatching ist der Algrorithmus std::advance aus der Standard Template Library. std::advance(it, n) inkrementiert den Iterator it um n Elemente:

// advanceTagDispatch.cpp

#include <iterator>
#include <forward_list>
#include <list>
#include <vector>
#include <iostream>

template <typename InputIterator, typename Distance>
void advance_impl(InputIterator& i, Distance n, std::input_iterator_tag) {
std::cout << "InputIterator used" << std::endl;
while (n--) ++i;
}

template <typename BidirectionalIterator, typename Distance>
void advance_impl(BidirectionalIterator& i, Distance n, std::bidirectional_iterator_tag) {
std::cout << "BidirectionalIterator used" << std::endl;
if (n >= 0)
while (n--) ++i;
else
while (n++) --i;
}

template <typename RandomAccessIterator, typename Distance>
void advance_impl(RandomAccessIterator& i, Distance n, std::random_access_iterator_tag) {
std::cout << "RandomAccessIterator used" << std::endl;
i += n;
}

template <typename InputIterator, typename Distance>
void advance_(InputIterator& i, Distance n) {
typename std::iterator_traits<InputIterator>::iterator_category category; // (1)
advance_impl(i, n, category); // (2)
}

int main(){

std::cout << std::endl;

std::vector<int> myVec{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto myVecIt = myVec.begin(); // (3)
std::cout << "*myVecIt: " << *myVecIt << std::endl;
advance_(myVecIt, 5);
std::cout << "*myVecIt: " << *myVecIt << std::endl;

std::cout << std::endl;

std::list<int> myList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto myListIt = myList.begin(); // (4)
std::cout << "*myListIt: " << *myListIt << std::endl;
advance_(myListIt, 5);
std::cout << "*myListIt: " << *myListIt << std::endl;

std::cout << std::endl;

std::forward_list<int> myForwardList{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto myForwardListIt = myForwardList.begin(); // (5)
std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;
advance_(myForwardListIt, 5);
std::cout << "*myForwardListIt: " << *myForwardListIt << std::endl;

std::cout << std::endl;

}

Der Ausdruck std::iterator_traits::iterator_category category bestimmt die Iterator-Kategorie zu Compilezeit. Basierend auf der Iterator-Kategorie wird die am besten passende Variante der Funktion advance_impl(i, n, category) in Zeile (2) verwendet. Jeder Container gibt den Iterator der Iterator-Kategorie zurück, die seiner Struktur entspricht. Daher ergibt die Zeile (3) einen Random Acccess Iterator, die Zeile (4) einen Bidirecional Iterator und die Zeile (5) einen Forward Iterator, der auch eine Input Iterator ist.

Aus dem Blickwinkel der Performanz betrachtet, ist diese Entscheidung sinnvoll, denn ein Random Access Iterator kann schneller als ein Bidrectional Iterator und ein Bidirectional Iterator kann schneller als eine Input Iterator inkrementiert werden. Aus dem Blickwinkel der Anwender betrachtet, wendest du std::advance(it, 5) an und erhälst die schnellste Variante, die der Container unterstützt.

Meine Erklärung war doch recht wortreich. Zu den verbleibenden zwei Regeln gibt es nicht so viel zu ergänzen.

T.25: Avoid complimentary constraints

Das Beispiel aus den Guidelines zeigt komplementäre Einschränkungen:

template<typename T> 
requires !C<T> // bad
void f();

template<typename T>
requires C<T>
void f();

Vermeide dies. Wende hingegen ein eingeschränktes und ein uneingeschränktes Template an:

template<typename T>   // general template
void f();

template<typename T> // specialization by concept
requires C<T>
void f();

Du kannst selbst das uneingeschränkte Template auf delete setzen, sodass sich nur noch die eingeschränkte Version verwenden lässt:

template<typename T>
void f() = delete;

T.26: Prefer to define concepts in terms of use-patterns rather than simple syntax

Der Titel für diese Regel ist ziemlich unspezifisch, aber das Bespiel liefert die notwendige Aufklärung. Anstelle die Concepts has_equal und has_not_equal um das concept Equalitity zu verwenden:

template<typename T> concept Equality = has_equal<T> && has_not_equal<T>;

solltest du das Use-Pattern vorziehen. Dadurch wird das folgende Beispiel deutlich lesbarer.

template<typename T> concept Equality = requires(T a, T b) {
bool == { a == b }
bool == { a != b }
// axiom { !(a == b) == (a != b) }
// axiom { a = b; => a == b } // => means "implies"
}

Das Concept fordert in diesem Fall, dass sich == und != auf die Argumente anwenden lassen und beide Funktionen bool zurückgebe sollten.

Wie geht's weiter?

Dies ist ein Teil der Einleitung der C++ Core Guidelines zu Templates: "...the interface to a template is a critical concept – a contract between a user and an implementer – and should be carefully designed". Im nächsten Artikel wird es kritisch.