Die Type-Traits-Bibliothek: std::is_base_of
Der letzte Artikel zu der Type-Traits-Bibliothek endete mit einer Herausforderung, und dieser Beitrag präsentiert die Antwort.
Der letzte Artikel zu der Type-Traits-Bibliothek endete mit einer Herausforderung, und dieser Beitrag präsentiert die Antwort.
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 [1]
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. ( [2])
URL dieses Artikels:
https://www.heise.de/-6283615
Links in diesem Artikel:
[1] https://en.cppreference.com/w/cpp/language/overload_resolution
[2] mailto:rainer@grimm-jaud.de
Copyright © 2021 Heise Medien