Die Type-Traits-Bibliothek: std::is_base_of

Modernes C++ Rainer Grimm  –  0 Kommentare

Der letzte Artikel zu der Type-Traits-Bibliothek endete mit einer Herausforderung, und dieser Beitrag präsentiert die Antwort.

Die Type-Traits Bibliothek: std::is_base_of

Bevor ich die Antwort von Herrn Zeisel vorstelle, möchte ich kurz die Herausforderung wiederholen.

Meine Herausforderung

Erklärt die beiden Implementierung der type-traits-Funktionen std::is_base_of und std::is_convertible.

  • 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)
> {};

Zugegeben, es gibt deutlich einfachere Herausforderungen. Daher habe ich nur eine sehr gute Antwort zu std::is_base_of erhalten. Es lohnt sich aber, die folgende Erklärung von Herrn Zeisel zu studieren, denn sie ist sehr lehrreich.

std::is_base_of

Program1.cpp

std::is_base_of beruht im Wesentlichen auf einigen Details der Regeln zu C++ Function Overload Resolution, die sich beispielsweise auf der C++-Referenzseite cppreference.com
finden. Die erste dabei verwendete Regel ist: "Conversion that converts pointer-to-derived to pointer-to-base is better than the conversion of pointer-to-derived to pointer-to-void,"

Ein Beispiel dazu ist Program1.cpp

// Program1.cpp

#include <iostream>
struct Base {};
struct Derived : public Base {};
struct A { };
// Conversion that converts pointer-to-derived to pointer-to-base
// is better than the conversion of pointer-to-derived to pointer-to-void,
// https://en.cppreference.com/w/cpp/language/overload_resolution
void f(void*)
{
std::cout << "f(void*)" << std::endl;
}
void f(const Base*)
{
std::cout << "f(Base*)" << std::endl;
}
int main()
{
Derived d;
A a;
f(&d);
f(&a);
return 0;
}

Der Output ist

f(Base*)
f(void*)
Program2.cpp

Mit dieser Regel lässt sich also ein Zeiger auf eine abgeleitete Klasse von einem anderen Zeiger
unterscheiden. Daraus lässt sich ein Type Trait wie im Program2.cpp konstruieren:

// Program2.cpp

#include <iostream>
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 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_ptr_convertible<Base>
(static_cast<Derived *>(nullptr)))::value
> { };
struct Base {};
struct Derived : public Base {};
struct A {};
int main()
{
std::cout << std::boolalpha;
std::cout << "Base is base of Derived: "
<< is_base_of<Base, Derived>::value << "\n";
std::cout << "Derived is base of Base: "
<< is_base_of<Derived, Base>::value << "\n";
std::cout << "Base is base of A: "
<< is_base_of<Base, A>::value << "\n";
std::cout << "Base is base of Base: "
<< is_base_of<Base, Base>::value << "\n";
std::cout << "Base is base of const Derived: "
<< is_base_of<Base, const Derived>::value << "\n";
std::cout << "int is base of int: "
<< is_base_of<int, int>::value << "\n";
std::cout << "void is base of void: "
<< is_base_of<void, void>::value << "\n";
std::cout << "void is base of Base: " < < is_base_of<void, Base>::value
<< "\n";
return 0;
}

test_pre_ptr_convertible sind zwei Funktionen mit unterschiedlichen Argument-Typen und
unterschiedlichen Typen der Rückgabewerte. Die Funktionen werden lediglich deklariert. Eine
Implementierung des Funktionsrumpfes ist nicht notwendig, da sie nie wirklich aufgerufen werden,
sondern lediglich zur Compilezeit der Typ des Rückgabewerts abgefragt wird:

test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr)

Ist Derived tatsächlich von Base abgeleitet, so wird die Funktion

test_pre_ptr_convertible(const volatile B*)

mit Rückgabetyp std::true_type ausgewählt; der Rückgabetyp wird mit decltype bestimmt und die zum Typ gehörende statische Variable value hat den Wert true.

Ist Derived nicht von Base abgeleitet, so wird die Funktion

test_pre_ptr_convertible(const volatile volatile*)

mit Rückgabetyp std::false_type ausgewählt und die entsprechende statische Variable value
hat den Wert false.

const volatile ist notwendig, damit ggf. auch const Derived oder volatile Derived als
von Base abgeleitet erkannt werden. In der Implementierung wird auch eine Klasse als Basis seiner
selbst angesehen, also is_base_of<Base,Base> liefert true.

Da Ableitung nur für Klassen Sinn hat, dient

std::is_class<Base>::value && std::is_class<Derived>::value

dazu, damit z.B

is_base_of<int,int>::value

false liefert.

Program3.cpp

Auf den ersten Blick schaut es so aus, als ob Program2.cpp bereits das Gewünschte leistet. Allerdings unterstützt C++ Mehrfachvererbung. Daher ist es möglich, dass eine Basisklasse mehrfach in der Ableitungshierarchie vorkommt. Das lässt sich mit Program3.cpp ausprobieren:

// Program3.cpp

#include <iostream>
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 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_ptr_convertible<Base>
(static_cast<Derived *>(nullptr)))::value
> {};
struct Base {};
struct Derived1 : public Base {};
struct Derived2 : public Base { };
struct Multi : public Derived1, public Derived2 { };
int main()
{
std::cout << std::boolalpha;
// error: ‘Base’ is an ambiguous base of ‘Multi’
std::cout << "Base is base of Multi: "
<< is_base_of<Base, Multi>::value << "\n";
return 0;
}

Der Compiler liefert jetzt die Fehlermeldung

error: ‘Base’ is an ambiguous base of ‘Multi’
Program4.cpp

Um hier wieder Eindeutigkeit zu bekommen, bietet sich SFINAE und ein extra Level Indirektion (in der Gestalt der Funktion test_pre_is_base_of) an:

// Program4.cpp

#include <iostream>
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() -> d
ecltype(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>())::value
> { };
struct Base {};
struct Derived1 : public Base {};
struct Derived2 : public Base {};
struct Multi : public Derived1, public Derived2 {};
int main()
{
std::cout << std::boolalpha;
std::cout << "Base is base of Multi: "
<< is_base_of<Base, Multi>::value << "\n";
// error: call of overloaded ‘test_pre_is_base_of<Derived2, Multi>()’
// is ambiguous
// std::cout << "Base is base of Derived1: "
//<< is_base_of<Base, Derived1>::value << "\n";
return 0;
}

Für den Funktionsaufruf

test_pre_is_base_of<Base,Multi>()

stehen die beiden Funktionen

template <typename B, typename D>
auto test_pre_is_base_of() ->
decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

und

    template <typename, typename>
auto test_pre_is_base_of() -> std::true_type;

zur Wahl. Der Funktionsaufruf

test_pre_ptr_convertible<Base>(static_cast<Multi*>(nullptr))

ruft

test_pre_ptr_convertible(const volatile Base*);

auf. Das ist aber zweideutig, da nicht klar ist, auf welche der beiden Base von Multi der Zeiger Base* zeigen soll. Das gibt also einen „Substitution Failure“. Da aber ein „Substitution Failure“ kein „Error“ ist, wird noch die andere Funktion

template <typename, typename>
auto test_pre_is_base_of() -> std::true_type;

überprüft. Da diese gültig ist, liefert

decltype(details::test_pre_is_base_of<Base,Multi>())::value

über diesen Weg den Wert true.

Leider funktioniert aber dieser Type Trait nicht mehr für einfache Basisklassen

is_base_of<Base, Derived1>::value

da in diesem Fall beide Funktionen

template <typename B, typename D>
auto test_pre_is_base_of() ->
decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

und

    template <typename, typename>
auto test_pre_is_base_of() -> std::true_type;

gültig und nach den Function Overload Resolution Regeln gleichwertig sind. Um dieses Problem zu lösen, muss daher irgendwie erzwungen werden, dass zuerst

template <typename B, typename D>
auto test_pre_is_base_of() ->
decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

gewählt wird, und

template <typename, typename>
auto test_pre_is_base_of() -> std::true_type;

nur dann gewählt wird, wenn die erste Funktion einen „Substitution Failure“ liefert.

Program5.cpp

Auch dafür gibt es eine Lösung:
„A standard conversion sequence is always better than a user-defined conversion sequence or an
ellipsis conversion sequence.“

// Program5.cpp

#include <iostream>
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)));
}
// A standard conversion sequence is always better
// than a user-defined conversion sequence
// or an ellipsis conversion sequence.
// https://en.cppreference.com/w/cpp/language/overload_resolution
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
> {};
struct Base {};
struct Derived1 : public Base {};
struct Derived2 : public Base {};
struct Multi : public Derived1, public Derived2 {};
int main()
{
std::cout << std::boolalpha;
std::cout << "Base is base of Derived1: "
<< is_base_of<Base, Derived1>::value << "\n";
std::cout << "Derived1 is base of Base: "
<< is_base_of<Derived1, Base>::value << "\n";
std::cout << "Base is base of Derived2: "
<< is_base_of<Base, Derived2>::value << "\n";
std::cout << "Derived2 is base of Base: "
<< is_base_of<Derived2, Base>::value << "\n";
std::cout << "Derived1 is base of Multi: "
<< is_base_of<Derived1, Multi>::value << "\n";
std::cout << "Derived2 is base of Multi: "
<< is_base_of<Derived2, Multi>::value << "\n";
std::cout << "Base is base of Multi: "
<< is_base_of<Base, Multi>::value << "\n";
return 0;
}

Verwendet man

template <typename B, typename D>
auto test_pre_is_base_of(int) ->
decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

(also eine „standard conversion“ zu int), und

template <typename, typename>
auto test_pre_is_base_of(...) -> std::true_type;

(also eine „ellipsis“), dann wird bevorzugt die erste Funktion (standard conversion) ausgewählt und
die zweite (ellispsis) tatsächlich nur im SFINAE Fall. Der Type Trait funktioniert damit also sowohl
für mehrfache als auch für einfache Basisklassen.

Wie geht's weiter?

Mit der Type-Traits Bibliothek lassen sich nicht nur Datentypen prüfen oder vergleichen sondern auch modifizieren. Genau damit beschäftigt sich mein nächster Artikel.