Die Type-Traits-Bibliothek: Typvergleiche

Nach dem Artikel zu Typprüfungen zur Compiletime mit der Type-Traits-Bibliothek folgen nun Typvergleichen beim Kompilieren.

Lesezeit: 8 Min.
In Pocket speichern
vorlesen Druckansicht Kommentare lesen 6 Beiträge
Von
  • Rainer Grimm
Inhaltsverzeichnis

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.

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.

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.

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.

Dank der Type-Traits Bibliothek kannst du Typen zur Compiletime verändern. Ich stelle diese Typmodifikation in meinem nächsten Beitrag genauer vor.

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++. ()