1

How `boost::is_base_of` matches private and/or ambiguous bases – Arthur O'Dwyer...

 11 months ago
source link: https://quuxplusone.github.io/blog/2023/10/06/boost-is-base-of/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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 <[email protected]>
(“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>()
> {};

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK