How boost::is_base_of matches private and/or ambiguous bases

The other day I learned that Boost.TypeTraits has a portable C++ implementation of std::is_base_of! You might think that is_base_of can be implemented simply as

template<class B> std::true_type f(B*);
template<class B> std::false_type f(void*);

template<class B, class D>
struct is_base_of : decltype(f<B>((D*)nullptr)) {};

(modulo some bells and whistles like stripping cv-qualifiers and verifying that both B and D are class types). However, this isn’t a full implementation of is_base_of’s standard semantics, because this implementation cannot handle private or ambiguous bases (Godbolt):

struct B {};
struct D1 : private B {};
static_assert(is_base_of<B, D1>::value); // hard error

template<class> struct Bx : B {};
struct D2 : Bx<int>, Bx<char> {};
static_assert(is_base_of<B, D2>::value); // hard error

Instead, Boost.TypeTraits’ implementation of is_base_of, formerly (pre-C++11) known as is_base_and_derived, works like this (modulo bells and whistles):

template<class B, class D>
struct Helper {
  template<class T = void>
  static std::true_type f(D*);  // #T
  static std::false_type f(B*); // #F
};

template<class B, class D>
struct Host {
  operator B*() const; // #B
  operator D*();       // #D
};

template<class B, class D>
struct is_base_of : decltype(
  Helper<B,D>::f(Host<B,D>())
) {};

Some historical explanations of how this code works are found in the Boost code’s comment block and in a few comp.lang.c++.moderated messages from the trick’s originator Rani Sharoni.

Sadly a lot of comp.lang.c++.moderated is now unarchived by Google, and the January/February 2003 period in which this technique was discussed predates the one public archive I’ve found, which covers only July 2003–October 2020. Rani Sharoni discussed a similar technique in a different context in <df893da6.0312250556.515613@posting.google.com>
(“Re: Is this valid -> part-2.”, December 25, 2003).)

Here’s my own attempt to explain what’s going on in this snippet.


Basically, there are four possible cases to consider here:

  • B and D are the same type.
  • B and D are unrelated types.
  • B is a (perhaps inaccessible and/or ambiguous) base class of D.
  • D is a (perhaps inaccessible and/or ambiguous) base class of B.

In the first case, B and D are the same type; so #T and #F have the same parameter list (except that #T is a template and #F isn’t). [over.match.best.general]/2.4 (C++20 DIS) says that we should prefer the non-template over the template. So if B and D are the same type, then our trait will yield false. (This is the wrong answer; but fixing this is easy. That’s one of the bells-and-whistles we’re omitting in this walkthrough.)

In the second case, B and D are unrelated types. Now we have two candidate functions #T and #F with different parameter lists: #T takes D* and #F takes B*. [over.match.best.general]/2 (C++20 DIS) tells us that to choose the better viable candidate we should compare the implicit conversion sequences (ICSes) that produce those parameter types out of the argument type Host<B,D>. The ICS producing D* is #D preceded by the identity conversion; the ICS producing B* is #B preceded by a conversion from Host<B,D> to const Host<B,D>&. Both ICSes are “user-defined conversion sequences,” which means neither is better than the other — user-defined conversion sequences are comparable only when their user-defined pieces are the same ([over.ics.rank]/3.3; C++20 DIS), and here #B and #D are different user-defined functions. So our two viable candidates (#T and #F) have equally good ICSes. Again [over.match.best.general]/2.4 (C++20 DIS) breaks the tie in favor of the non-template, which is #F. Our trait yields false, which is the correct answer in this case.

In the third case, B is a base class of D. Again #T takes D*, #F takes B*, and we need to find the ICSes that produce those parameter types from Host<B,D>. The ICS producing D* is still #D preceded by the identity conversion. For the ICS producing B*, we have a choice! We could use the same ICS as above — #B preceded by a conversion from Host<B,D> to const Host<B,D>& ([over.ics.ref]/1; C++20 DIS) — or we could use #D followed by a conversion from D* to B* ([conv.ptr]/3; C++20 DIS). To choose between these two user-defined conversion functions, [over.match.conv]/1 (C++20 DIS) says that we recursively do overload resolution between #B and #D. To use #B, we’d have to apply an identity conversion from Host<B,D> to const Host<B,D>&. To use #D, we’d have to apply a no-op identity conversion, which is better because it’s less qualified ([over.ics.rank]/3.2.6, C++20 DIS). Therefore, we prefer #D. Okay, to recap: We’re doing overload resolution between #T and #F. The ICS producing #T’s D* (we now know) is just #D. The best ICS producing #F’s B* is #D followed by a standard conversion from D* to B*. The former is a subsequence of the latter (or, in the words of [over.ics.rank]/3.3 (C++20 DIS), their user-defined pieces are the same and the former’s second standard conversion (the identity conversion) is better than the latter’s), so overload resolution will prefer the former and choose #T. Our trait yields true, which is the correct answer in this case.

In the fourth case, D is a base class of B. Again #T takes D*, #F takes B*, and we need to find the ICSes that produce those parameter types from Host<B,D>. The ICS producing B* is #B preceded by a conversion from Host<B,D> to const Host<B,D>&. For the ICS producing D*, we have a choice! We could use #B preceded by a conversion from Host<B,D> to const Host<B,D>& and followed by a standard conversion from B* to D* (remember, in this case D is a base of B, not vice versa), or we could just use #D directly. Obviously we’ll prefer #D directly. So, the ICS producing #T’s D* is #D; the ICS producing #F’s B* is #B preceded by a conversion from Host<B,D> to const Host<B,D>&. As in our second case, their user-defined pieces are different, therefore neither one is a better conversion sequence than the other ([over.ics.rank]/3.3; C++20 DIS). Again [over.match.best.general]/2.4 (C++20 DIS) breaks the tie in favor of the non-template #F. Our trait yields false, which is the correct answer in this case.

Notice that in no case do we ever wind up actually evaluating a pointer conversion from D* to B*, so it never matters that base B might be inaccessible and/or ambiguous. In each case where such a conversion is possible, by definition we end up calling #T, whose parameter type is D*.


Boost’s is_base_of relies on [over.match.best.general]/2.4 (C++20 DIS) to break ties in cases 2 and 4. We could substitute another tiebreaker, e.g. the new-in-C++20 [over.match.best]/2.9 (C++20 DIS), which says that a non-reversed rewritten candidate should be preferred over a reversed one. (Godbolt.)

template<class B, class D>
struct Helper {
  friend constexpr int operator<=>(Helper, D*) { return 1; }
  friend constexpr int operator<=>(B*, Helper) { return 0; }
};

template<class B, class D>
struct Host {
  constexpr operator B*() const { return nullptr; }
  constexpr operator D*() { return nullptr; }
};

template<class B, class D>
struct is_base_of : std::bool_constant<
  Host<B,D>() < Helper<B,D>()
> {};

Boost’s is_base_of uses a qualification conversion to make #B less preferred than #D in cases 3 and 4. We can substitute another standard conversion from the table in [over.ics.scs], e.g. the derived-to-base conversion, if only we can figure out how to shoehorn it into a user-defined conversion function like operator B*. I don’t think that’s possible pre-C++23 because of [over.match.funcs.general]/4.2 (C++20 DIS); but in C++23, we can do it via an explicit object parameter (Godbolt).

template<class B, class D>
struct Helper {
  friend constexpr int operator<=>(Helper, D*) { return 1; } // #T
  friend constexpr int operator<=>(B*, Helper) { return 0; } // #F
};

struct HostBase {};

template<class B, class D>
struct Host : HostBase {
  constexpr operator B*(this HostBase) { return nullptr; } // #B
  constexpr operator D*(this Host) { return nullptr; } // #D
};

template<class B, class D>
struct is_base_of : std::bool_constant<
  Host<B,D>() < Helper<B,D>()
> {};
Posted 2023-10-06