templatesTypeTraits

The Type-Traits Library: std::is_base_of

I finished the last article on the Type-Traits library with the challenge of explaining the std::is_base_of and std::is_convertible functions. Today I’m excited to present the answer from Mr. Helmut Zeisel.

templatesTypeTraits

Before I present Mr. Zeisel’s response, I would like to repeat the challenge briefly.

My challenge

Explain the two implementations of the type-traits functions std::is_base_of and 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<
        boolean,
        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)
> {};

 

Admittedly, there are significantly simpler challenges. Therefore, I only got one perfect answer to std::is_base_of. However, it is worth studying the following explanation by Mr. Zeisel, as it is very instructive. I translated his German explanation into English, and I kept his layout.

std::is_base_of

Program1.cpp

std::is_base_of is essentially based on some details of the C++ Function Overload Resolution rules, which can be found, for example, at https://en.cppreference.com/w/cpp/language/overload_resolution. for example. The first rule used in this is: “Conversion that converts pointer-to-derived to pointer-to-base is better than the conversion of pointer-to-derived to pointer-to-void,”

 

Rainer D 6 P2 500x500Modernes C++ Mentoring

  • "Fundamentals for C++ Professionals" (open)
  • "Design Patterns and Architectural Patterns with C++" (open)
  • "C++20: Get the Details" (open)
  • "Concurrency with Modern C++" (open)
  • "Generic Programming (Templates) with C++": October 2024
  • "Embedded Programming with Modern C++": October 2024
  • "Clean Code: Best Practices for Modern C++": March 2025
  • Do you want to stay informed: Subscribe.

     

    An example of this is 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;
    }
    

     

    The output is

    f(base*)
    f(void*)
    

     

    Program2.cpp

    This rule can distinguish a pointer to a derived class from another pointer. From this, a type trait can be constructed as in Program2.cpp:

     

    // 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 are two functions with different argument types and different types of return values. The functions are declared. An implementation of the function body is not necessary since they are never actually called, but only at compile time the type of the return value is queried: test_pre_ptr_convertible<Base>(static_cast<Derived*>(nullptr). If Derived is derived from Base, the function test_pre_ptr_convertible(const volatile B*) with return type std::true_type is selected; the return type is determined with decltype and the static variable value associated with the type has the value true. If Derived is not derived from Base, the function test_pre_ptr_convertible(const volatile volatile*) with return type std::false_type is selected, and the corresponding static variable value has the value false.
    const volatile is necessary so that const Derived or volatile Derived can be recognized as derived from base. In the implementation, a class is also considered as a base of its itself, so is_base_of<base,base> returns true.
    Since derivation only makes sense for classes, the following is used std::is_class<Base>::value && std::is_class<Derived>::value so that e.g. is_base_of<int,int>::value returns false.

    Program3.cpp

    At first glance, it looks like Program2.cpp already does what it should. However, C++ supports multiple inheritances. Therefore a base class may occur multiple times in the derivation hierarchy. This can be tested with Program3.cpp:

     

    // 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; }

     

    The compiler now returns the error message
    error: 'Base' is an ambiguous base of 'Multi'

    Program4.cpp

    To get unambiguousness again, SFINAE and an extra level of indirection (in the form of the function test_pre_is_base_of) are useful:

    // 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() -> 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>())::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; }

     

    For the function call
    test_pre_is_base_of<base,multi>()
    the two functions
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

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

    can be choosen. The function call
    test_pre_ptr_convertible<base>(static_cast<multi*>(nullptr))
    calls
    test_pre_ptr_convertible(const volatile Base*);
    . But this is ambiguous since it is not clear to which of the two bases of Multi the pointer Base* should point. So this gives a “substitution failure”. But since a “substitution failure” is not an “error”, the other function
    template <typename, typename>
         auto test_pre_is_base_of() -> std::true_type;

    is checked. This is valid, so it returns
    decltype(details::test_pre_is_base_of<base,multi>())::value
    returns the value true via this path.
    Unfortunately, however, this type trait no longer works for simple base classes
    is_base_of<base,derived1>::value
    because in this case both functions
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

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

    are valid and equivalent according to the Function Overload Resolution rules. Therefore, to solve this problem, it is necessary to somehow enforce that first
    template <typename B, typename D>
        auto test_pre_is_base_of() ->
            decltype(test_pre_ptr_convertible<B>(static_cast<D*>(nullptr)));

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

    is chosen only if the first function returns a substitution failure.

    Program5.cpp

    A solution for this also exists: “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; }

     

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

    (i.e. a “standard conversion” to int), and
    template <typename, typename>
        auto test_pre_is_base_of(...) -> std::true_type;

    (i.e. an “ellipsis”), then the first function (standard conversion) is selected preferentially and the second (ellipsis) actually only in the SFINAE case. So the type trait works both for multiple as well as for simple base classes.

    What’s next?

    With the type traits library, you can check or compare types and modify them. This is exactly what my next article will deal with.

     

     

    Thanks a lot to my Patreon Supporters: Matt Braun, Roman Postanciuc, Tobias Zindl, G Prvulovic, Reinhold Dröge, Abernitzke, Frank Grimm, Sakib, Broeserl, António Pina, Sergey Agafyin, Андрей Бурмистров, Jake, GS, Lawton Shoemake, Jozo Leko, John Breland, Venkat Nandam, Jose Francisco, Douglas Tinkham, Kuchlong Kuchlong, Robert Blanch, Truels Wissneth, Mario Luoni, Friedrich Huber, lennonli, Pramod Tikare Muralidhara, Peter Ware, Daniel Hufschläger, Alessandro Pezzato, Bob Perry, Satish Vangipuram, Andi Ireland, Richard Ohnemus, Michael Dunsky, Leo Goodstadt, John Wiederhirn, Yacob Cohen-Arazi, Florian Tischler, Robin Furness, Michael Young, Holger Detering, Bernd Mühlhaus, Stephen Kelley, Kyle Dean, Tusar Palauri, Juan Dent, George Liao, Daniel Ceperley, Jon T Hess, Stephen Totten, Wolfgang Fütterer, Matthias Grün, Phillip Diekmann, Ben Atakora, Ann Shatoff, Rob North, Bhavith C Achar, Marco Parri Empoli, Philipp Lenk, Charles-Jianye Chen, Keith Jeffery, Matt Godbolt, and Honey Sukesan.

    Thanks, in particular, to Jon Hess, Lakshman, Christian Wittenhorst, Sherhy Pyton, Dendi Suhubdy, Sudhakar Belagurusamy, Richard Sargeant, Rusty Fleming, John Nebel, Mipko, Alicja Kaminska, Slavko Radman, and David Poole.

    My special thanks to Embarcadero
    My special thanks to PVS-Studio
    My special thanks to Tipi.build 
    My special thanks to Take Up Code
    My special thanks to SHAVEDYAKS

    Modernes C++ GmbH

    Modernes C++ Mentoring (English)

    Do you want to stay informed about my mentoring programs? Subscribe Here

    Rainer Grimm
    Yalovastraße 20
    72108 Rottenburg

    Mobil: +49 176 5506 5086
    Mail: schulung@ModernesCpp.de
    Mentoring: www.ModernesCpp.org

    Modernes C++ Mentoring,

     

     

    0 replies

    Leave a Reply

    Want to join the discussion?
    Feel free to contribute!

    Leave a Reply

    Your email address will not be published. Required fields are marked *