C++ Core Guidelines: Programmierung zur Compilezeit mit der Type-Traits-Bibliothek (Die Zweite)

Modernes C++  –  0 Kommentare

Die Type-Traits-Bibliothek unterstützt Typprüfungen, Typvergleiche und Typmodifikationen zur Compilezeit. Genau: Heute geht es um Typmodifikationen zur Compilezeit.

Die Type-Traits-Bibliothek

Es mag verwunderlich klingen, aber Typmodifikationen ist die Domäne von Template-Metaprogrammierung und daher auch der Type-Traits-Bibliothek.

Typmodifikationen

Ich denke, du bist neugierig, was zur Compilezeit möglich ist: viel! Hier sind die spannendsten Metafunktionen.

// const-volatile modifications:
remove_const;
remove_volatile;
remove_cv;
add_const;
add_volatile;
add_cv;

// reference modifications:
remove_reference;
add_lvalue_reference;
add_rvalue_reference;

// sign modifications:
make_signed;
make_unsigned;

// pointer modifications:
remove_pointer;
add_pointer;

// other transformations:
decay;
enable_if;
conditional;
common_type;
underlying_type;

Um einen int von einem int oder einem const int zu erhalten, reicht eine einfache Anfrage nach dem Type mit ::type.

int main(){

std::is_same<int, std::remove_const<int>::type>::value; // true
std::is_same<int, std::remove_const<const int>::type>::value;// true

}

Seit C++14 gibt die Metafunktion direkt mit _t wie bei std::remove_const_t ihren Typ preis.

int main(){

std::is_same<int, std::remove_const_t<int>>::value; // true
std::is_same<int, std::remove_const_t<const int>>::value; // true
}

Um eine Idee zu bekommen, wie mächtig diese Metafunktionen sind, sind hier ein paar Anwendungsfälle:

  • remove_reference: std::move und std::forward verwendet diese Funktion, um die Referenz von seinem Argument zu entfernen. Hier ist std::move in einer Zeile.
    • static_cast<std::remove_reference<decltype(arg)>::type&&>(arg);
  • decay: std::thread wendet std::decay auf seine Argumente an. Diese ist die Funktion f, die er ausführt und die Funktionsargumente args. Decay steht für die implizite Konvertierung von Arrays zu Zeigern, Funktion zu Funktionszeigern und das Entfernen der const/volatile-Qualifizierer und Referenzen.
    • std::invoke(decay_copy(std::forward<Function>(f)),
      decay_copy(std::forward<Args>(args))...);
  • enable_if: std::enable_if ist eine einfache Form, SFINAE anzuwenden. SFINAE steht für Substitution Failure Is Not An Error und wird während der Überladung von Funktions-Templates angewandt. Es bedeutet: Wenn bei der Ersetzung der Template-Parameter ein Fehler auftritt, ist dies kein Fehler. Diese Instantiierung der Template-Parameter wird aus der Menge aller Funktionsüberladungen einfach entfernt. std::enable_if wird sehr häufig in std::tuple verwendet.
  • conditional: std::conditional ist der ternäre Operator zur Compilezeit.
  • common_type: std::common_type bestimmt den gemeinsamen Datentyp einer Menge von Datentypen.
  • underlying_type: std::underlying_type ermittelt den Datentyp einer Aufzählung.

Eventuell habe ich dich noch nicht überzeugt, warum die Type-Traits so wichtig sind? Daher will ich meine Geschichte zu den Type-Traits mit ihren zwei größten Vorteilen beenden: Korrektheit und Performanz.

Korrektheit

Korrektheit bedeutet einerseits, dass die Type-Traits-Bibliothek dazu verwendet werden kann, Concepts wie Integral, SignedIntegral oder UnsignedIntegral zu implementieren.

template <class T>
concept bool Integral() {
return is_integral<T>::value;
}

template <class T>
concept bool SignedIntegral() {
return Integral<T>() && is_signed<T>::value;
}

template <class T>
concept bool UnsignedIntegral() {
return Integral<T>() && !SignedIntegral<T>();
}

Korrektheit bedeutet auch, dass sie eingesetzt werden kann, um Algorithmen typsicherer anzubieten. Ich wendete in meinem Artikel Immer sicherer die Funktionen std::is_integral, std::conditional, std::common_type und std::enable_if der Type-Traits-Bibliothek an um den gcd-Algorithmus sukzessiv sicherer zu implementieren.

Damit du eine bessere Idee von dem Artikel Immer sicherer erhältst: Hier ist der Startpunkt meines generischen gcd-Algorithmus.

// 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)= " << gcd(100, 10) << std::endl;
std::cout << "gcd(100, 33)= " << gcd(100, 33) << std::endl;
std::cout << "gcd(100, 0)= " << gcd(100, 0) << std::endl;

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

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

std::cout << std::endl;

}

Die Ausgabe des Programms bringt zwei Probleme ans Tageslicht.

Erstens ist die Verwendung des Modulo-Operators für den Datentype double in Zeile 1 und den C-String in Zeile 2 nicht gültig. Zweitens, sollte die Verwendung des Datentyps int und long int in Zeile 3 möglich sein. Beide Probleme können elegant mit der Type-Traits-Bibliothek gelöst werden.

Bei der Type-Traits-Bibliothek geht es nicht nur um Korrektheit, es geht auch um Optimierung.

Optimierung

Die zentrale Idee der Type-Traits-Bibliothek ist einfach. Der Compiler analysiert die verwendeten Datentypen und trifft auf Grundlage seiner Analyse eine Entscheidung, welcher Code ausgeführt werden soll. Im Falle der Algorithmen std::copy, std::fill oder std::equal der Standard Template Library bedeutet dies, dass die Algorithmen entweder auf jedes Element des Bereiches oder auf den ganzen Speicherbereich sukzessive angewandt werden. Im zweiten Fall kommen dann die schnellen C-Funktionen memset, memcpy, oder memmove zum Einsatz. Im Gegensatz zu memcpy erlaubt memmove überlappende Speicherbereiche.

// fill  
// Specialization: for char types we can use memset.
template<typename _Tp>
inline typename
__gnu_cxx::__enable_if<__is_byte<_Tp>::__value, void>::__type // (1)
__fill_a(_Tp* __first, _Tp* __last, const _Tp& __c)
{
const _Tp __tmp = __c;
if (const size_t __len = __last - __first)
__builtin_memset(__first, static_cast<unsigned char>(__tmp), __len);
}

// copy

template<bool _IsMove, typename _II, typename _OI>
inline _OI
__copy_move_a(_II __first, _II __last, _OI __result)
{
typedef typename iterator_traits<_II>::value_type _ValueTypeI;
typedef typename iterator_traits<_OI>::value_type _ValueTypeO;
typedef typename iterator_traits<_II>::iterator_category _Category;
const bool __simple = (__is_trivial(_ValueTypeI) // (2)
&& __is_pointer<_II>::__value
&& __is_pointer<_OI>::__value
&& __are_same<_ValueTypeI, _ValueTypeO>::__value);

return std::__copy_move<_IsMove, __simple,
}

// lexicographical_compare

template<typename _II1, typename _II2>
inline bool
__lexicographical_compare_aux(_II1 __first1, _II1 __last1,
_II2 __first2, _II2 __last2)
{
typedef typename iterator_traits<_II1>::value_type _ValueType1;
typedef typename iterator_traits<_II2>::value_type _ValueType2;
const bool __simple = // (3)
(__is_byte<_ValueType1>::__value && __is_byte<_ValueType2>::__value
&& !__gnu_cxx::__numeric_traits<_ValueType1>::__is_signed
&& !__gnu_cxx::__numeric_traits<_ValueType2>::__is_signed
&& __is_pointer<_II1>::__value
&& __is_pointer<_II2>::__value);

return std::__lexicographical_compare<__simple>::__lc(__first1, __last1,
__first2, __last2);
}

Die Zeilen 1, 2 und 3 zeigen, dass die Type-Traits-Bibliothek zum Einsatz kommt, um besseren Code zu erzeugen. In meinem Artikel Type-Traits Performanz gehe ich tiefer auf diese Optimierung ein und präsentiere beeindruckende Performanzzahlen für den GCC und MSVC.

Wie geht's weiter?

Mit constexpr entflieht die Programmierung der Compilezeit ihrer Expertennische und wird zum Mainstream. constexpr ist Programierung zur Compilezeit mit der gewohnten C++-Syntax.

C++-Schulungen im Großraum Stuttgart

Ich freue mich darauf, weitere C++-Schulungen halten zu dürfen.

Die Details zu meinen C++- und Python-Schulungen gibt es auf www.ModernesCpp.de.