How Does 'Is_Base_Of' Work

How does `is_base_of` work?

If they are related

Let's for a moment assume that B is actually a base of D. Then for the call to check, both versions are viable because Host can be converted to D* and B*. It's a user defined conversion sequence as described by 13.3.3.1.2 from Host<B, D> to D* and B* respectively. For finding conversion functions that can convert the class, the following candidate functions are synthesized for the first check function according to 13.3.1.5/1

D* (Host<B, D>&)

The first conversion function isn't a candidate, because B* can't be converted to D*.

For the second function, the following candidates exist:

B* (Host<B, D> const&)
D* (Host<B, D>&)

Those are the two conversion function candidates that take the host object. The first takes it by const reference, and the second doesn't. Thus the second is a better match for the non-const *this object (the implied object argument) by 13.3.3.2/3b1sb4 and is used to convert to B* for the second check function.

If you would remove the const, we would have the following candidates

B* (Host<B, D>&)
D* (Host<B, D>&)

This would mean that we can't select by constness anymore. In an ordinary overload resolution scenario, the call would now be ambiguous because normally the return type won't participate in overload resolution. For conversion functions, however, there is a backdoor. If two conversion functions are equally good, then the return type of them decides who is best according to 13.3.3/1. Thus, if you would remove the const, then the first would be taken, because B* converts better to B* than D* to B*.

Now what user defined conversion sequence is better? The one for the second or the first check function? The rule is that user defined conversion sequences can only be compared if they use the same conversion function or constructor according to 13.3.3.2/3b2. This is exactly the case here: Both use the second conversion function. Notice that thus the const is important because it forces the compiler to take the second conversion function.

Since we can compare them - which one is better? The rule is that the better conversion from the return type of the conversion function to the destination type wins (again by 13.3.3.2/3b2). In this case, D* converts better to D* than to B*. Thus the first function is selected and we recognize the inheritance!

Notice that since we never needed to actually convert to a base class, we can thereby recognize private inheritance because whether we can convert from a D* to a B* isn't dependent on the form of inheritance according to 4.10/3

If they are not related

Now let's assume they are not related by inheritance. Thus for the first function we have the following candidates

D* (Host<B, D>&) 

And for the second we now have another set

B* (Host<B, D> const&)

Since we cannot convert D* to B* if we haven't got a inheritance relationship, we now have no common conversion function among the two user defined conversion sequences! Thus, we would be ambiguous if not for the fact that the first function is a template. Templates are second choice when there is a non-template function that is equally good according to 13.3.3/1. Thus, we select the non-template function (second one) and we recognize that there is no inheritance between B and D!

Logic of std::is_base_of in C++ 11

When B is a base class of D, the call Test(static_cast<D*>(0)) resolves to Yes Test(B*). Otherwise, it resolves to No Test(...).

If B is a base class of D, the value of sizeof(Test(static_cast<D*>(0))) is sizeof(Yes). Otherwise, it is equal to sizeof(No).

Yes and No are defined such that sizeof(Yes) will never be equal to sizeof(No).

If B is a base class of D,

sizeof(Test(static_cast<D*>(0))) == sizeof(Yes)

evaluates to true. Otherwise it evaluates to false.

Understanding a reimplementation of std::is_base_of

  1. It seems to me that IsDerivedFromHelper defines two static members of the same name (Test)? How is this possible?

Those member functions with the same name have different parameter lists. This is called function overloading.


  1. The classes No and Yes both define no constructor, meaning they only have the default no-arguments constructor. If so, how are we able to pass B* (or ...) to Test()?

The return type of the function has no effect on what parameters can be passed into it.


  1. I understand that ... means variadic arguments. But - what does it mean in this situation?

Variadic arguments.


  1. Again, instantiating Test with an argument of type D* where the type of Test doesn't define a constructor.

A function is called, not instantiated. Functions do not have constructors.

What is going on here?

A member function Test is called. The call may resolve to either overload depending on the template type arguments.

“is_base_of” inside a class declaration

Seems the only difference is that it does not handle non-public inheritance.

Thanks to Axalo for the comment

Explanation of possible implementation of std::is_base_of

  1. template <typename Base> std::true_type is_base_of_test_func( Base* );

When the argument is a Base, or derived from Base, this overload has the highest priority


  1. template <typename Base> std::false_type is_base_of_test_func( void* );

this overload will match any type, with the lowest priority


  1. template <typename Base, typename Derived>
    using pre_is_base_of = decltype( is_base_of_test_func<Base>( std::declval<Derived*>() ) );

pre_is_base_of will become the type returned by calling is_base_of_test_func with a pointer to Derived. If Derived is derived from Base, it will return std::true_type, otherwise the void* overload will be selected and it will return a std::false_type. Now we have converted a function call result into a type.


  1. template <typename Base, typename Derived, typename = void>
    struct pre_is_base_of2 : public std::true_type {};

General case, this will be a true_type. Since the 3rd template argument is defaulted, this will be the version of the class defined when no other specialisation is created.


  1. template<typename ...> using void_t = void;

This is an easier way of doing enable_if. void_t<X> will only be a type if X is a legal type.


  1. template <typename Base, typename Derived>
    struct pre_is_base_of2<Base, Derived, void_t<pre_is_base_of<Base, Derived>>> : public pre_is_base_of<Base, Derived>{};

if void_t is a legal type (i.e. pre_is_base_of<Base>(Derived*) is a valid expression, this will be the specialisation of pre_is_base_of2, which will evaluate to the decltype of calling the test function, above. It will only be selected if pre_is_base_of<Base,Derived> is a valid type (i.e. there exists a call to the test function)


  1. template <typename Base, typename Derived>
    struct is_base_of : public std::conditional_t<std::is_class<Base>::value && std::is_class<Derived>::value,
    pre_is_base_of2<Base, Derived>,
    std::false_type>
    {
    };

essentially this is saying:

IF Base and Value are classes AND void_t<decltype(is_base_of_test_func<Base>(Derived*))> is a type
THEN
select the type of pre_is_base_of2<Base, Derived, void_t<...is the expression legal?...>>
ELSE
select false_type

Update:

Hopefully this little demo program will provide some clarity:

#include <type_traits>
#include <iostream>

template<class...> using void_t = void;

// this expands in any case where no second type is provided
template<class T, typename = void> struct does_he_take_sugar : std::false_type {};

// the specialisation can only be valid when void_t<expr> evaluates to a type.
// i.e. when T has a member function called take_sugar
template<class T> struct does_he_take_sugar<T, void_t<decltype(std::declval<T>().take_sugar())>> : std::true_type {};


struct X {
int take_sugar();
};

struct Y {
int does_not();
};

int main()
{

// X::take_sugar is a function therefore void_t<decltype(...X)> will evaluate to void
std::cout << does_he_take_sugar<X>::value << std::endl;

// Y::take_sugar is not a function therefore void_t<decltype(...Y)> will not evaluate at all
std::cout << does_he_take_sugar<Y>::value << std::endl;

// int::take_sugar is not even valid c++ void_t<decltype(...int)> will not evaluate at all
std::cout << does_he_take_sugar<int>::value << std::endl;
}

is_base_of of generic type

From commentary of @PiotrSkotnicki

template <template <typename...> class Base, typename Derived>
struct is_base_of_template
{
using U = typename std::remove_cv<Derived>::type;

template <typename... Args>
static std::true_type test(Base<Args...>*);

static std::false_type test(void*);

using type = decltype(test(std::declval<U*>()));
};

template <template <typename...> class Base, typename Derived>
using is_base_of_template_t = typename is_base_of_template<Base, Derived>::type;

This solution works fine, example.

Conversion functions, std::is_base_of and spurious incomplete types: substitution failure IS an error

This isn’t SFINAE, which certainly applies to incomplete types:

template<class T,bool=false>
struct size : std::integral_constant<std::size_t,0> {};
template<class T>
struct size<T,!sizeof(T)> : std::integral_constant<std::size_t,sizeof(T)> {};
static_assert(size<char>::value==1); // OK

struct A;
static_assert(size<A>::value==0); // OK
struct A {};
static_assert(size<A>::value==0); // OK!

The issue here is that std::is_base_of has “undefined behavior” (read: use of it makes a program ill-formed, no diagnostic required) if both types are non-union class types and the latter is incomplete. This isn’t covered by SFINAE both because it’s not in the immediate context and because it’s NDR.

These compilers are choosing to reject the code, which has the benefit of preventing surprises like the // OK! above.

You can use the direct SFINAE approach to avoid this problem:

template< typename T >
class MyClass: public MyClassBase {
public:
template< typename U, std::enable_if_t<
(sizeof(U), std::is_base_of<MyClassBase, U>::value), bool> = true >
operator U () const {
return U{}; // Complex initialization omitted for brevity
}
};

Here there is a direct substitution error in the type of the template parameter if U is incomplete, which prevents instantiating the std::is_base_of specialization that would be IFNDR. Note that using incomplete types with SFINAE this way is still rather dangerous, because the same specialization instantiated in a context where the types in question are complete might produce different behavior (which is also IFNDR, per [temp.point]/7).

Interdependent class template and std::is_base_of specialization

std::is_base_of requires complete type. which is not the case in

template <typename T>
struct derived : base<T>
{
child<derived> child_; // derived<T> not yet complete here.
typedef T type;
};

For T::base_tag, IIRC (I think POI of child<derived> move from before struct derived to current location in the class), T doesn't need to be complete, and only visited part of it is visible.

So derived::type would not be visible. (but derived::base::type would be).

std::is_base_of for template classes

You may do the following:

template <template <typename...> class C, typename...Ts>
std::true_type is_base_of_template_impl(const C<Ts...>*);

template <template <typename...> class C>
std::false_type is_base_of_template_impl(...);

template <typename T, template <typename...> class C>
using is_base_of_template = decltype(is_base_of_template_impl<C>(std::declval<T*>()));

Live Demo

but will fail with multiple inheritance or private inheritance from A.

With Visual Studio 2017 this will fail when the base class template has more than one template parameter, and it is unable to deduce Ts...

Demo

VS Bug Report

Refactoring solves the problem for VS.

template < template <typename...> class base,typename derived>
struct is_base_of_template_impl
{
template<typename... Ts>
static constexpr std::true_type test(const base<Ts...> *);
static constexpr std::false_type test(...);
using type = decltype(test(std::declval<derived*>()));
};

template < template <typename...> class base,typename derived>
using is_base_of_template = typename is_base_of_template_impl<base,derived>::type;

Live Demo



Related Topics



Leave a reply



Submit