Mit der Type-Traits Bibliothek lassen sich Typen vergleichen und modifizieren. Die Bibliothek agiert zur Compilezeit und besitzt damit keinen Einfluss auf die Laufzeit des Programms.
Typen vergleichen
Die Type-Traits Bibliothek kennt drei Typvergleiche:
- is_base_of<Derived,Base>
- is_convertible<From,To>
- is_same<T,U>
Die Klassen-Templates geben mit Hilfe ihres Mitglied value true oder false zurück und sind damit wieder ideale Kandidaten für static_assert.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
// 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;
// static_assert(std::is_base_of<Derived,Base>::value,"Derived is not base of Base");
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;
// static_assert(std::is_convertible<Base*,Derived*>::value,"Base* can not be converted to Derived*");
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;
// static_assert(std::is_same<int, int64_t>::value,"int is not the same type as int64_t");
std::cout << std::endl;
}
|
Die Ausgabe des Programms sollte kein großes Überraschungspotential bergen.
Kommentiere ich static_assert in den Zeilen 18, 26 und 34 ein, so schlagen die Zusicherungen zur Compilezeit zu.
Typen modifizeren
Nun bin ich ein bisschen pingelig. Auch wenn der C++-Standard vom Modifizieren oder Transformieren von Typen spricht, so ist das nicht ganz richtig. Zur Compilezeit gibt es keinen Zustand. Daher kann auch nichts modifiziert oder verändert werden. Zur Compilezeit kann nur ein neuer Typ auf Bedarf erzeugt werden. Der Grund ist, dass die Type-Traits Bibliothek Template Metaprogrammierung in einem besonders schönen Gewand verkörpert. Template Metaprogrammierung ist eine rein funktionale Subsprache, die in die C++-Sprache eingebettet ist. Rein funktionale Sprachen kennen keinen Zustand. Um das Verwirrungspotential aber niedrig zu halten, werde ich im Rest des Artikels weiter vom Modifizieren von Typen sprechen.
Die Type-Traits Bibliothek bittet einen reichen Satz an Funktionen, um Typen zur Compilierungszeit zu modifizieren. So lassen sich die const oder volatile Eigenschaften eines Typs entfernen oder hinzufügen, lässt sich das Vorzeichen eines Typs oder die Dimension eines Arrays ändern und auch die Zeiger- oder Referenzeigenschaften eines Typs modifizieren. Hier kommt der Überblick:
// const-volatile modifications
template <class T> struct remove_const;
template <class T> struct remove_volatile;
template <class T> struct remove_cv;
template <class T> struct add_const;
template <class T> struct add_volatile;
template <class T> struct add_cv;
// reference modifications
template <class T> struct remove_reference;
template <class T> struct add_lvalue_reference;
template <class T> struct add_rvalue_reference;
// sign modifications
template <class T> struct make_signed;
template <class T> struct make_unsigned;
// array modifications
template <class T> struct remove_extent;
template <class T> struct remove_all_extents;
// pointer modifications
template <class T> struct remove_pointer;
template <class T> struct add_pointer;
Um von einer Referenz int& zur Compilezeit den einfachen Typ ohne Referenz zu erhalten, muss das Mitglied type des Klassen-Templates verwendet werden. In C++14 ist dies deutlich einfacher. Hier ist es ausreichend, an die Funktion _t anzuhängen. Dies gilt für alle vorgestellten Funktionen dieses Abschnitts.
std::cout << std::is_same<int,std::remove_reference<int &>::type>::value << std::endl; // true
std::cout << std::is_same<int,std::remove_reference_t<int &>>::value << std::endl; // true
Der entscheidende Punkt an den kleinen Codeschnipsel ist, dass std::remove_reference<int &>::type mit C++14 einfacher in der Form std::remove_reference_t<int &> geschrieben werden kann. Der Wert des Vergleichs mit std::is_same steht dank value zur Verfügung.
Der Vollständigkeit dieses Artikels willens muss ich noch erwähnen. Nun sollte ich die weiteren verschiedene Transformationen wie die Funktionen std::conditional, std::common_type und std::enable_if vorstellen. Wiederholen will ich mich aber nicht. Den drei zitiereten Funktionen habe ich bereits den Artikel Statisch geprüft gewidmet. Der Rest lässt sich schön unter Miscellaneous transformations auf der Seite von cppreference.com nachlesen.
Eine Frage bleibt noch bestehen.
Wie funktioniert die ganze Magie?
Mit einem bisschen Template Metaprogammierung sind die Klassen-Templates is_same und remove_const schnell implementiert. Zur besseren Unterscheidung vom Namensraum std verwende ich in bekannter Manier den Namensraum rgr.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
// removeConst.cpp
#include <iostream>
#include <string>
#include <type_traits>
namespace rgr{
template<class T, class U>
struct is_same : std::false_type {};
template<class T>
struct is_same<T, T> : std::true_type {};
template< class T >
struct remove_const{
typedef T type;
};
template< class T >
struct remove_const<const T> {
typedef T type;
};
}
int main(){
std::cout << std::boolalpha << std::endl;
std::cout << std::is_same<int,std::remove_const<const int>::type>::value << std::endl;
std::cout << rgr::is_same<int,rgr::remove_const<const int>::type>::value << std::endl;
typedef rgr::remove_const<double>::type myDouble;
std::cout << rgr::is_same<double,myDouble>::value << std::endl;
typedef rgr::remove_const<const std::string>::type myString;
std::cout << rgr::is_same<std::string,myString>::value << std::endl;
typedef rgr::remove_const<std::add_const<int>::type>::type myInt;
std::cout << rgr::is_same<int,myInt>::value << std::endl;
std::cout << std::endl;
}
|
In dem Namensraum rgr habe ich is_same und remove_const umgesetzt. Diese entspricht der Implementierung der Type-Traits Bibliothek. Dabei greife ich der Einfachheit halber auf die statischen Konstanten std::false_type und std::true_type in den Zeilen 10 und 14 zurück, die ich im Artikel Typeigenschaften abfragen vorgestellt habe. Dank der Basisklasse std::false_type gibt das Klassen-Template auf die Anfrage ::value false zurück, dank der Basisklasse std::true_type gibt das Klassen-Template true zurück. Die entscheidende Beobachtung bei dem Klassen-Template is_same ist es, das allgemeine Template (Zeile 9 und 10) vom teilweise spezialisierten Template (Zeile 12 und 13) zu unterscheiden. Der Compiler wählt das teilweise spezialisierte Template genau dann aus, wenn die beiden Template-Argumente den gleichen Typ besitzen. Genau dies zeigt die Signatur des teilweise spezialisierten Templates in den Zeilen 12 und 13. is_same besitzt im Gegensatz zum allgemeinen Klassen-Template nur einen Typ-Parameter T. Die Argumentation für das Klassen-Template remove_cost ist ähnlich. Das allgemeine Template gibt mittels seines Mitglieds type den Typ unverändert zurück, das teilweise spezialisierte Template gibt den Typ zurück, nachdem seine const-Eigenschaft (Zeile 22) entfernt wurde. Das teilweise spezialisierte Template wird vom Compiler genau dann verwendet, wenn sein Template-Argument const ist.
Der Rest des Programms ist schnell erklärt. In den Zeilen 31 und 32 kommen die Funktionen der Type-Traits Bibliothek und meine implementierten Funktionen zum Einsatz. Mittels typedef erkläre ich einen Typ myDouble (Zeile 34), einen Typ myString (Zeile 37) und einen Typ myInt. Alle Typen sind nicht konstant.
Zum Abschluss noch die Ausgabe des Programms.
Wie geht's weiter?
Eine wichtige Eigenschaft der Type-Traits Bibliothek habe ich absichtlich ignoriert. Im ersten Schritt lässt sich mit ihren Funktionen das Typ-System zur Compilezeit analysieren, so dass im zweiten Schritt auf die Typen optimierte ausführbare Programme entstehen. Das geht auch einfacher. Die Type-Traits Bibliothek ermöglicht es, sich selbst optimierende Programme zu schreiben. Wie das Ganze geht, folgt in einem Artikel zur Performanz. Im nächsten Artikel werde ich benutzerdefinierten Literalen vorstellen. Mein LIeblingsfeature, wenn es um sicherheitskritische Software geht. Benutzerdefinierte Literale sind Literale, die das Rechnen mit Einheiten erlauben. Der Compiler sorgt in diesem Fall dafür, dass nicht Äpfel mit Birnen verrechnet werden.
Was du schon immer wissen wolltest
- Template Metaprogrammierung
- Im dem Artikel Magischer Mechanismus 01/2011 für das Linux-Magazin stelle ich Template-Metaprogrammierung mit C++ genauer vor.
- Funktionale Programmierung
- In dem Artikel Funktionale Programmierung (1) 09/2009 für das Linux-Magazin Online stelle ich die Grundzüge der funktionalen Programmierung vor.
Go to Leanpub/cpplibrary "What every professional C++ programmer should know about the C++ standard library". Hole dir dein E-Book. Unterstütze meinen Blog.
Weiterlesen...