Die Type-Traits-Bibliothek: Typvergleiche
Nach dem Artikel zu TypprĂĽfungen zur Compiletime mit der Type-Traits-Bibliothek folgen nun Typvergleichen beim Kompilieren.
- Rainer Grimm
Nach dem Artikel zu TypprĂĽfungen zur Compiletime mit der Type-Traits-Bibliothek folgen nun Typvergleichen beim Kompilieren.
Die Type-Traits Bibliothek ermöglicht es, Typen zur Compiletime vergleichen, sodass sie nicht die Laufzeit belasten.
Typvergleiche
Die Type-Traits Bibliothek unterstĂĽtzt in C++11 drei Arten von Vergleichen:
- is_base_of<Base, Derived>
- is_convertible<From, To>
- is_same<T, U>
Mit C++20 haben wir zwei zusätzliche Funktionen:
- is_pointer_interconvertible_with_class<From, To>
- is_pointer_interconvertible_base_of<Base, Derived>
Der Einfachheit halber schreibe ich nur ĂĽber die C++11-Metafunktionen.
Dank ihres Attributes gibt jedes Template true
oder false
zurück und ist daher die optimale Ergänzung zu static_assert.
// compare.cpp
#include <cstdint>
#include <iostream>
#include <type_traits>
class Base{};
class Derived: public Base{};
int main(){
std::cout << std::boolalpha << '\n';
std::cout << "std::is_base_of<Base,Derived>::value: "
<< std::is_base_of<Base,Derived>::value << '\n';
std::cout << "std::is_base_of<Derived,Base>::value: "
<< std::is_base_of<Derived,Base>::value << '\n';
std::cout << "std::is_base_of<Derived,Derived>::value: "
<< std::is_base_of<Derived,Derived>::value << '\n';
// static_assert(std::is_base_of<Derived,Base>::value,
// "Derived is not base of Base"); // (1)
std::cout << '\n';
std::cout << "std::is_convertible<Base*,Derived*>::value: "
<< std::is_convertible<Base*,Derived*>::value << '\n';
std::cout << "std::is_convertible<Derived*,Base*>::value: "
<< std::is_convertible<Derived*,Base*>::value << '\n';
std::cout << "std::is_convertible<Derived*,Derived*>::value: "
<< std::is_convertible<Derived*,Derived*>::value << '\n';
// static_assert(std::is_convertible<Base*,Derived*>::value,
"Base* can not be converted to Derived*"); // (2)
std::cout << '\n';
std::cout << "std::is_same<int, int32_t>::value: "
<< std::is_same<int, int32_t>::value << '\n';
std::cout << "std::is_same<int, int64_t>::value: "
<< std::is_same<int, int64_t>::value << '\n';
std::cout << "std::is_same<long int, int64_t>::value: "
<< std::is_same<long int, int64_t>::value << '\n';
// static_assert(std::is_same<int, int64_t>::value,
"int is not the same type as int64_t"); // (3)
std::cout << '\n';
}
Die Ausgabe des Programms sollte nicht ĂĽberraschen.
Wenn ich static_assert
in (1), (2) und (3) verwende, wird die Zusicherung zur Compiletime ausgelöst:
Die Website cppreference.com enthält mögliche Implementierungen aller Meta-Funktionen std::is_base_of,
std::is_convertible und std::is_same.
Es ist sehr interessant und herausfordernd, sie zu studieren.
Implementierung
Hier sind zunächst einmal drei Implementierungen der drei Metafunktionen. Ich beginne mit der einfachsten:
std::is_same
Im folgenden Beispiel verwende ich den Namensraum rgr
, um meine Implementierung von der C++ Standardimplementierung zu unterscheiden.
// isSame.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, class U>
struct is_same : false_type {}; // (3)
template<class T>
struct is_same<T, T> : true_type {};
}
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
std::cout << "rgr::is_same<int, const int>::value: "
<< rgr::is_same<int, const int>::value << '\n'; // (1)
std::cout << "rgr::is_same<int, volatile int>::value: "
<< rgr::is_same<int, volatile int>::value << '\n';
std::cout << "rgr::is_same<int, int>::value: "
<< rgr::is_same<int, int>::value << '\n';
std::cout << '\n';
std::cout << "std::is_same<int, const int>::value: "
<< std::is_same<int, const int>::value << '\n';
std::cout << "std::is_same<int, volatile int>::value: "
<< std::is_same<int, volatile int>::value << '\n';
std::cout << "std::is_same<int, int>::value: "
<< std::is_same<int, int>::value << '\n';
std::cout << '\n';
}
Zunächst eine kurze Erinnerung: Der Aufruf des Funktions-Templates rgr::is_same<int, const int>
(1) bewirkt den Aufruf des Ausdrucks rgr::false_type::value
(2), da std::is_same<>
von false_type
(3) abgeleitet ist. rgr::false_type::value
ist ein Alias fĂĽr rgr::integral_constant<bool, false>::value
(2). Ich verwende im Beispiel den statischen constexpr
Wert der Klasse integral_constant. integral_constant
ist die Basisklasse der type-traits Funktionen.
Zwei Fakten sind bei folgender Ausgabe interessant. Meine Implementierungen rgr::is_same
verhalten sich wie std::is_same
und const
und volatile
sind Teil des Typs.
Es ist recht einfach, die Metafunktion isSameIgnoringConstVolatile
auf der Grundlage der Metafunktion is_same
zu implementieren.
// isSameIgnoringConstVolatile.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;
typedef integral_constant<bool, false> false_type;
template<class T, class U>
struct is_same : false_type {};
template<class T>
struct is_same<T, T> : true_type {};
template<typename T, typename U> // (1)
struct isSameIgnoringConstVolatile: rgr::integral_constant<
bool,
rgr::is_same<typename std::remove_cv<T>::type,
typename std::remove_cv<U>::type>::value
> {};
}
int main() {
std::cout << '\n';
std::cout << std::boolalpha;
std::cout << "rgr::isSameIgnoringConstVolatile<int, const int>::value: "
<< rgr::isSameIgnoringConstVolatile<int, const int>::value
<< '\n';
std::cout << "rgr::is_same<int, volatile int>::value: "
<< rgr::isSameIgnoringConstVolatile<int, volatile int>::value
<< '\n';
std::cout << "rgr::isSameIgnoringConstVolatile<int, int>::value: "
<< rgr::isSameIgnoringConstVolatile<int, int>::value << '\n';
std::cout << '\n';
}
Die Metafunktion isSameIgnoringConstVolatile
leitet sich von rgr::integral_constant
ab und verwendet die Funktion std::remove_cv
, um const
oder volatile
aus ihren Typen zu entfernen. std::remove_cv
ist eine Funktion aus der Type-Traits Bibliothek und ermöglicht es, Typen zur Compiletime zu ändern. Ich werde in meinem nächsten Beitrag mehr über die Modifikation von Typen schreiben.
Hier ist die Ausgabe des Programms:
Schauen wir uns die beiden Metafunktionen std::is_base_of
und std::is_convertible
genauer an. Hier sind mögliche Implementierungen von ihnen direkt von cppreference.com.
std::is_base_of
namespace details {
template <typename B>
std::true_type test_pre_ptr_convertible(const volatile B*);
template <typename>
std::false_type test_pre_ptr_convertible(const volatile void*);
template <typename, typename>
auto test_pre_is_base_of(...) -> std::true_type;
template <typename B, typename D>
auto test_pre_is_base_of(int) ->
decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));
}
template <typename Base, typename Derived>
struct is_base_of :
std::integral_constant<
bool,
std::is_class<Base>::value && std::is_class<Derived>::value &&
decltype(details::test_pre_is_base_of<Base, Derived>(0))::value
> { };
std::is_convertible
namespace detail {
template<class T>
auto test_returnable(int) -> decltype(
void(static_cast<T(*)()>(nullptr)), std::true_type{}
);
template<class>
auto test_returnable(...) -> std::false_type;
template<class From, class To>
auto test_implicitly_convertible(int) -> decltype(
void(std::declval<void(&)(To)>()(std::declval<From>())), std::true_type{}
);
template<class, class>
auto test_implicitly_convertible(...) -> std::false_type;
} // namespace detail
template<class From, class To>
struct is_convertible : std::integral_constant<bool,
(decltype(detail::test_returnable<To>(0))::value &&
decltype(detail::test_implicitly_convertible<From, To>(0))::value) ||
(std::is_void<From>::value && std::is_void<To>::value)
> {};
Das ist der Grund, warum ich std::is_same
erklärt habe. Zum Abschluss folgt eine kleine Challenge.
Meine Herausforderung
Erklärt die beiden Implementierung der type-traits Funktionen std::is_base_of
und std::is_convertible
. Schickt eure Erklärung bis Donnerstag (2. Dezember) an Rainer.Grimm@ModernesCpp.de. Die beste Antwort für jede Funktion erhält einen Gutschein für mein LeanPub-Bundle Modern C++ Collection.
Ich veröffentliche die beste Antwort zu jeder Funktion in meinem nächsten Beitrag und nenne die Vornamen der Einsender. Wenn ich deinen vollständigen Namen nennen soll, schreibe mir dies bitte.
Wie geht's weiter?
Dank der Type-Traits Bibliothek kannst du Typen zur Compiletime verändern. Ich stelle diese Typmodifikation in meinem nächsten Beitrag genauer vor.
Online-Schulung: Clean Code: Best Practices fĂĽr modernes C++
Ich freue mich darauf, meine nächste Online-Schulung vom 14.12 - 16.12.2021 durchzuführen. Jeder Teilnehmer erhält eines meiner Bücher zur freien Auswahl und einen Gutschein für meinen Onlinekurs C++ Fundamentals for Professionals. Die Schulung findet definitiv statt.
Mehr Informationen zu meinen Schulung gibt es hier: Modernes C++. ()