C++ Core Guidelines: Programmierung zur Compilezeit mit der Type-Traits-Bibliothek

Modernes C++  –  4 Kommentare

Die Reise durch die Programmierung zur Compilezeit geht weiter, und zwar mit einem Sprung von C++98 nach C++11 hin zur Type-Traits-Bibliothek, die Template-Metaprogrammierung in einer standardisierten Form verkörpert.

Die Type-Traits-Bibliothek gehört seit C++11 zu C++. Ihre Ursprünge liegen in Boost. Sie erlaubt es, zur Compilezeit Typeigenschaften abzufragen, Typen zu vergleichen und zu modifizieren. Die Bibliothek besitzt mehr als 100 Funktionen, und jeder weitere Standard fügt neue hinzu.

Ich möchte weder in die Tiefe der Type-Traits-Bibliothek abtauchen, da ich bereits einige Artikel über sie geschrieben habe, noch werde ich sie auslassen. Als Kompromiss schreibe ich eine kurze Einführung zur Type-Traits-Bibliothek. Für weiterführende Informationen füge ich Links zu meinen bestehenden Artikel hinzu.

Zuerst einmal, was ist in der Type-Traits-Bibliothek drin?

Die Type-Traits-Bibliothek

Die Bibliothek erlaubt, Typeigenschaften abzufragen, Typen zu vergleichen und zu modifizieren. Los geht der Artikel mit dem Abfragen von Typeigenschaften:

Typeigenschaften abfragen

Jeder Datentyp gehört genau zu einer der vierzehn primären Typkategorien.

Primäre Typkategorien

Dies sind die vierzehn primäre Typkategorien:

template <class T> struct is_void;
template <class T> struct is_integral;
template <class T> struct is_floating_point;
template <class T> struct is_array;
template <class T> struct is_pointer;
template <class T> struct is_null_pointer;
template <class T> struct is_member_object_pointer;
template <class T> struct is_member_function_pointer;
template <class T> struct is_enum;
template <class T> struct is_union;
template <class T> struct is_class;
template <class T> struct is_function;
template <class T> struct is_lvalue_reference;
template <class T> struct is_rvalue_reference;

Das folgende Programm besitzt zu jeder primären Typkategorie einen passenden Datentyp:

//  primaryTypeCategories.cpp

#include <iostream>
#include <type_traits>

struct A{
int a;
int f(int){return 2011;}
};

enum E{
e= 1,
};

union U{
int u;
};


int main(){

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

std::cout << std::is_void<void>::value << std::endl;
std::cout << std::is_integral<short>::value << std::endl;
std::cout << std::is_floating_point<double>::value << std::endl;
std::cout << std::is_array<int [] >::value << std::endl;
std::cout << std::is_pointer<int*>::value << std::endl;
std::cout << std::is_null_pointer<std::nullptr_t>::value << std::endl;
std::cout << std::is_member_object_pointer<int A::*>::value << std::endl;
std::cout << std::is_member_function_pointer<int (A::*)(int)>::value << std::endl;
std::cout << std::is_enum<E>::value << std::endl;
std::cout << std::is_union<U>::value << std::endl;
std::cout << std::is_class<std::string>::value << std::endl;
std::cout << std::is_function<int * (double)>::value << std::endl;
std::cout << std::is_lvalue_reference<int&>::value << std::endl;
std::cout << std::is_rvalue_reference<int&&>::value << std::endl;

std::cout << std::endl;

}

Hier ist die Ausgabe des Programms:

Falls du wissen willst, wie diese Magie passiert: Mein Artikel "Typeigenschaften abfragen" klärt auf.

Auf den primären Typkategorien basieren die zusammengesetzten Typkategorien.

Zusammengesetzte Typkategorien

Die Tabelle zeigt die Beziehungen zwischen den primären und den zusammengesetzten Typkategorien:

Die Type-Traits-Bibliothek erlaubt es, noch weitere Typeigenschaften abzufragen:

template <class T> struct is_const;
template <class T> struct is_volatile;
template <class T> struct is_trivial;
template <class T> struct is_trivially_copyable;
template <class T> struct is_standard_layout;
template <class T> struct is_pod;
template <class T> struct is_literal_type;
template <class T> struct is_empty;
template <class T> struct is_polymorphic;
template <class T> struct is_abstract;
template <class T> struct is_signed;
template <class T> struct is_unsigned;
template <class T, class... Args> struct is_constructible;
template <class T> struct is_default_constructible;
template <class T> struct is_copy_constructible;
template <class T> struct is_move_constructible;
template <class T, class U> struct is_assignable;
template <class T> struct is_copy_assignable;
template <class T> struct is_move_assignable;
template <class T> struct is_destructible;
template <class T, class... Args> struct is_trivially_constructible;
template <class T> struct is_trivially_default_constructible;
template <class T> struct is_trivially_copy_constructible;
template <class T> struct is_trivially_move_constructible;
template <class T, class U> struct is_trivially_assignable;
template <class T> struct is_trivially_copy_assignable;
template <class T> struct is_trivially_move_assignable;
template <class T> struct is_trivially_destructible;
template <class T, class... Args> struct is_nothrow_constructible;
template <class T> struct is_nothrow_default_constructible;
template <class T> struct is_nothrow_copy_constructible;
template <class T> struct is_nothrow_move_constructible;
template <class T, class U> struct is_nothrow_assignable;
template <class T> struct is_nothrow_copy_assignable;
template <class T> struct is_nothrow_move_assignable;
template <class T> struct is_nothrow_destructible;
template <class T> struct has_virtual_destructor;

Viele der Funktionen wie is_trivially_copyable besitzen die Namenskomponente trivially. Das bedeutet, dass diese Funktionen nicht vom Entwickler implementiert wurden. Wird eine Methode vom Compiler mit default angefordert, gilt diese als trivial.

Typvergleiche

Die Type-Traits-Bibliothek bietet drei verschieden Vergleiche an:

    is_base_of<Base, Derived>
is_convertible<From, To>
is_same<T, U>

Das folgende Programm verwendet alle drei Funktionen:

// compare.cpp

#include <cstdint>
#include <iostream>
#include <type_traits>

class Base{};
class Derived: public Base{};

int main(){

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

std::cout << "std::is_base_of<Base, Derived>::value: "
<< std::is_base_of<Base, Derived>::value << std::endl;
std::cout << "std::is_base_of<Derived, Base>::value: "
<< std::is_base_of<Derived, Base>::value << std::endl;
std::cout << "std::is_base_of<Derived, Derived>::value: "
<< std::is_base_of<Derived, Derived>::value << std::endl;

std::cout << std::endl;

std::cout << "std::is_convertible<Base*, Derived*>::value: "
<< std::is_convertible<Base*, Derived*>::value << std::endl;
std::cout << "std::is_convertible<Derived*, Base*>::value: "
<< std::is_convertible<Derived*, Base*>::value << std::endl;
std::cout << "std::is_convertible<Derived*,Derived*>::value: "
<< std::is_convertible<Derived*, Derived*>::value << std::endl;

std::cout << std::endl;

std::cout << "std::is_same<int, int32_t>::value: "
<< std::is_same<int, int32_t>::value << std::endl;
std::cout << "std::is_same<int, int64_t>::value: "
<< std::is_same<int, int64_t>::value << std::endl;
std::cout << "std::is_same<long int, int64_t>::value: "
<< std::is_same<long int, int64_t>::value << std::endl;

std::cout << std::endl;

}

und besitzt die erwartete Ausgabe.

Programmierung zur Compilezeit

Nun möchte ich ein paar Schritte zurücktreten und über die Funktionen der Type-Traits-Bibliothek nachdenken. Hier sind meine Beobachtungen.

  • Die Funktionen der Type-Traits-Bibliothek sind Metafunktion, denn sie werden zur Compilezeit ausgeführt. In diesem Fall sind sie Klassen-Templates.
  • Die Argumente der Metafunktionen in den eckigen Klammern (<...>) sind Metadaten. Genau genommen sind es in unseren Beispielen Datentypen.
  • Der Rückgabewert der Funktion ist ::value. Dies ist nur ein Alias. Seit C++17 gibt es eine einfache Form das Ergebnis zu erhalten: Anstelle von std::in_void<void>::value lässt sich einfach std::is_void_v<void> schreiben.

Ich denke, meine drei Beobachtungen erinnern dich an meinen letzten Artikel. Dies sind genau die Konventionen, dich ich in meinen letzten Artikel zur Template Metaprogrammierung vorgestellt habe: "C++ Core Guidelines: Programmierung zur Compilezeit".

Wie geht's weiter?

Falls die Funktionen der Type-Traits-Bibliothek einen Datentyp und nicht einen Wert zurückgeben sollen, dann muss ::type aufgerufen werden. Mein nächster Artikel zeigt, welche Typmodifikationen zur Compilezeit die Type-Traits-Bibliothek unterstützt. Letztlich verfolgt die Type-Traits-Bibliothek zwei große Ziele: Korrektheit und Optimierung.