Die Type-Traits Bibliothek: Typprüfungen

Modernes C++ Rainer Grimm  –  69 Kommentare

Die Type-Traits Bibliothek ist Bestandteil von C++11 und unterstützt Typprüfungen, Typvergleiche und Typänderungen zur Compiletime. Die Bibliothek umfasst mehr als 100 Funktionen und wächst mit jeder neuen C++-Standardversion.

In diesem Artikel stelle ich Typprüfungen mir der Type-Traits Bibliothek genauer vor.

Die Type-Traits Bibliothek: Typprüfungen

Typprüfung

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

Primäre Typkategorien

Hier sind alle Kategorien:

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 stellt für jede primäre Typkategorie einen Datentyp vor:

// 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() {

using namespace std;

cout << boolalpha << '\n';

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

}
Wie funktioniert dieser Zauber?

Diese Technik basiert auf Templates und Template-Spezialisierung, ein paar Konventionen und einer Menge Tipparbeit. Ich habe eine vereinfachte Version des Funktions-Templates std::integral implementiert. std::integral prüft, ob ein gegebener Typ ein integral Typ ist. Ich ignoriere dabei const oder volatile Qualifier.

// integral.cpp

#include <iostream>
#include <type_traits>

namespace rgr{

template<class T, T v>
struct integral_constant {
static constexpr T value = v;
typedef T value_type;
typedef integral_constant type;
constexpr operator value_type() const noexcept { return value; }
//since c++14:
constexpr value_type operator()() const noexcept { return value; }
};

typedef integral_constant<bool, true> true_type; // (2)
typedef integral_constant<bool, false> false_type;

template <class T>
struct is_integral : public false_type{};

template <>
struct is_integral<bool> : public true_type{};

template <>
struct is_integral<char> : public true_type{};

template <>
struct is_integral<signed char> : public true_type{};

template <>
struct is_integral<unsigned char> : public true_type{};

template <>
struct is_integral<wchar_t> : public true_type{};

template <>
struct is_integral<short> : public true_type{};

template <>
struct is_integral<int> : public true_type{}; // (3)

template <>
struct is_integral<long> : public true_type{};

template <>
struct is_integral<long long> : public true_type{};

template <>
struct is_integral<unsigned short> : public true_type{};

template <>
struct is_integral<unsigned int> : public true_type{};

template <>
struct is_integral<unsigned long> : public true_type{};

template <>
struct is_integral<unsigned long long> : public true_type{};

}

int main(){

std::cout << std::boolalpha << '\n';

std::cout << "std::is_integral<int>::value: "
<< std::is_integral<int>::value << '\n';
std::cout << "rgr::is_integral<int>::value: "
<< rgr::is_integral<int>::value << '\n'; // (1)

std::cout << "std::is_integral<double>::value: "
<< std::is_integral<double>::value << '\n';
std::cout << "rgr::is_integral<double>::value: "
<< rgr::is_integral<double>::value << '\n';

std::cout << '\n';

std::cout << "std::true_type::value: " << std::true_type::value << '\n';
std::cout << "rgr::true_type::value: " << rgr::true_type::value << '\n';

std::cout << "std::false_type::value: " << std::false_type::value << '\n';
std::cout << "rgr::false_type::value: " << rgr::false_type::value << '\n';

std::cout << '\n';

std::cout << "std::integral_constant<bool, true>::value: "
<< std::integral_constant<bool, true>::value << '\n';
std::cout << "rgr::integral_constant<bool, true>::value: "
<< rgr::integral_constant<bool, true>::value << '\n';

std::cout << "std::integral_constant<bool, false>::value: "
<< std::integral_constant<bool, false>::value << '\n';
std::cout << "rgr::integral_constant<bool, false>::value: "
<< rgr::integral_constant<bool, false>::value << '\n';

std::cout << '\n';

}

Ich verwende in meiner Implementierung den Namespace rgr und vergleiche die Funktionen mit den entsprechenden Type-Traits-Funktionen im Namespace std. Der Aufruf der Funktionsvorlage rgr::is_integral<int>::value (1) bewirkt den Aufruf des Ausdrucks rgr::true_type::value (2), weil integral<int> von true_type (3) abgeleitet ist. rgr::true_type::value ist ein Alias für rgr::integral_constant<bool, true>::value (2). Im Beispiel verwende ich den statischen constexpr value der Klasse integral_constant. integral_constant ist die Basisklasse der Type-Traits-Funktionen.

Der Vollständigkeit halber ist hier die Ausgabe des Programms. Meine Implementierung liefert die gleichen Ergebnisse, wie die Funktionen aus der Type-Traits Bibliothek.

Die Type-Traits Bibliothek: Typprüfungen


Ich habe in meinem Funktions-Template rgr::is_integral ::value als Rückgabe verwendet. Hier möchte ich an diese Konvention aus meinem vorherigen Beitrag "Template Metaprogrammierung: Wie es funktioniert" erinnern: Meine Funktions-Templates rgr::is_integral sind Metafunktionen und sie verwenden die Namenskonventionen der Template-Metaprogrammierung. Seit C++17 gibt es für Konventionen eine Hilfsklasse für ::value. Diese Hilfsklasse basiert auf Variablen-Templates.

template< Klasse T >
inline constexpr bool is_integral_v = is_integral<T>::value

Variablen-Templates stellen im Wesentlichen eine Familie von Variablen dar. Dank dieser Hilfsklasse kann man std::integral_v<T> anstelle von std::integral<T>::value verwenden. Diese verkürzte Schreibweise funktioniert für alle Funktions-Templates der Type-Traits Library.

Zusammengesetzte Typkategorien werden aus den primären Typkategorien zusammengesetzt.

Zusammengesetzte Typkategorien

Es gibt sieben zusammengesetzte Typkategorien. Die folgende Tabelle zeigt sie.

Die Type-Traits Bibliothek: Typprüfungen

Zusätzlich zu den primären Typkategorien und den zusammengesetzten Typkategorien bietet die Type-Traits Bibliothek Typeneigenschaften und die Abfrage von Typeneigenschaften an. Der Vollständigkeit halber sind sie hier aufgeführt.

Typ-Eigenschaften
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_empty;
template <class T> struct is_polymorphic;
template <class T> struct is_abstract;
template <class T> struct is_final;
template <class T> struct is_aggregate;

template <class T> struct is_signed;
template <class T> struct is_unsigned;
template <class T> struct is_bounded_array;
template <class T> struct is_unbounded_array;
template <class T> struct is_scoped_enum;

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, class U> struct is_swappable_with;
template <class T> struct is_swappable;

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, class U> struct is_nothrow_swappable_with;
template <class T> struct is_nothrow_swappable;

template <class T> struct is_nothrow_destructible;

template <class T> struct has_virtual_destructor;

template <class T> struct has_unique_object_representations;

Viele der Metafunktionen wie std::is_trivially_copyable haben "trivial" in ihrem Namen. Das bedeutet, dass der Compiler diese Methode bereitstellt. Eine Methode vom Compiler mit dem Schlüsselwort default anzufordern, ist ebenfalls trivial.

Typ-Eigenschaftsabfragen

template <class T> struct alignment_of;
template <class T> struct rank;
template <class T, unsigned I = 0> struct extent;

Wie geht's weiter?

Auffällig Ist die Funktion std::is_same in der zusammengesetzten Typkategorie std::is_fundamental: std::is_same ist etwas Besonderes, weil sie Typvergleiche zur Compiletime ermöglicht. Darüber werde ich in meinem nächsten Artikel schreiben.