Как работает `is_base_of`?

Как работает следующий код?

typedef char (&yes)[1];
typedef char (&no)[2];

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

template <typename B, typename D>
struct is_base_of
{
  template <typename T> 
  static yes check(D*, T);
  static no check(B*, int);

  static const bool value = sizeof(check(Host<B,D>(), int())) == sizeof(yes);
};

//Test sample
class Base {};
class Derived : private Base {};

//Expression is true.
int test[is_base_of<Base,Derived>::value && !is_base_of<Derived,Base>::value];
  • Обратите внимание, что B является частной базой. Как это работает?

  • Обратите внимание, что operator B*() является константой. Почему это важно?

  • Почему template<typename T> static yes check(D*, T); лучше, чем static yes check(B*, int);?

Примечание. Это сокращенная версия (макросы удаляются) из boost::is_base_of. И это работает в широком диапазоне компиляторов.

Ответ 1

Если они связаны

Пусть на мгновение предположим, что B на самом деле является базой D. Тогда для вызова check обе версии жизнеспособны, потому что Host можно преобразовать в D* и B*. Это пользовательская последовательность преобразований, описанная 13.3.3.1.2 от Host<B, D> до D* и B* соответственно. Для нахождения функций преобразования, которые могут преобразовать класс, для первой функции check синтезируются следующие кандидатные функции в соответствии с 13.3.1.5/1

D* (Host<B, D>&)

Первая функция преобразования не является кандидатом, потому что B* не может быть преобразован в D*.

Для второй функции существуют следующие кандидаты:

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

Это два кандидата функции преобразования, которые принимают объект-хост. Первый принимает его по ссылке const, а второй - нет. Таким образом, второе является лучшим совпадением для объекта non-const *this (аргумент подразумеваемого объекта) на 13.3.3.2/3b1sb4 и используется для преобразования в B* для второй функции check.

Если вы удалите const, у нас будут следующие кандидаты

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

Это означает, что мы больше не можем выбирать константу. В обычном сценарии разрешения перегрузки вызов теперь будет неоднозначным, поскольку обычно тип возврата не будет участвовать в разрешении перегрузки. Однако для функций преобразования есть бэкдор. Если две функции преобразования одинаково хороши, тогда тип возврата определяет, кто лучше всего соответствует 13.3.3/1. Таким образом, если вы удалите константу, то первая будет принята, потому что B* лучше преобразуется в B*, чем D* в B*.

Теперь, какая пользовательская последовательность преобразований лучше? Один для второй или первой функции проверки? Правило состоит в том, что пользовательские последовательности преобразования могут сравниваться только в том случае, если они используют одну и ту же функцию преобразования или конструктор согласно 13.3.3.2/3b2. Это как раз здесь: обе используют вторую функцию преобразования. Обратите внимание, что при этом const важно, потому что он заставляет компилятор взять вторую функцию преобразования.

Так как мы можем сравнить их - какой из них лучше? Правило заключается в том, что выигрывает лучшее преобразование из возвращаемого типа функции преобразования в тип назначения (опять-таки на 13.3.3.2/3b2). В этом случае D* лучше преобразуется к D*, чем к B*. Таким образом, выбирается первая функция, и мы наследуем наследование!

Обратите внимание: поскольку нам никогда не нужно было преобразовывать базовый класс, мы можем тем самым распознать личное наследование, потому что, если мы можем преобразовать из D* в B*, это не зависит от вид наследования согласно 4.10/3

Если они не связаны

Теперь предположим, что они не связаны наследованием. Таким образом, для первой функции мы имеем следующие кандидаты:

D* (Host<B, D>&) 

А для второго мы имеем теперь еще один набор

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

Поскольку мы не можем преобразовать D* в B*, если у нас нет отношения наследования, теперь у нас нет общей функции преобразования среди двух пользовательских последовательностей преобразования! Таким образом, мы были бы неоднозначными, если бы не тот факт, что первая функция является шаблоном. Шаблоны являются вторым выбором, если есть функция без шаблона, которая одинаково хороша в соответствии с 13.3.3/1. Таким образом, мы выбираем функцию без шаблона (вторая) и признаем, что наследование между B и D!

отсутствует.

Ответ 2

Давайте рассмотрим, как это работает, просмотрев шаги.

Начните с части sizeof(check(Host<B,D>(), int())). Компилятор может быстро увидеть, что этот check(...) является выражением вызова функции, поэтому ему нужно сделать разрешение перегрузки на check. Доступны две потенциальные перегрузки, template <typename T> yes check(D*, T); и no check(B*, int);. Если выбрано первое, вы получите sizeof(yes), else sizeof(no)

Затем рассмотрим разрешение перегрузки. Первой перегрузкой является экземпляр шаблона check<int> (D*, T=int), а второй кандидат - check(B*, int). Фактические аргументы приведены Host<B,D> и int(). Второй параметр явно не различает их; он просто помог сделать первую перегрузку шаблоном. Позже мы увидим, почему важна часть шаблона.

Теперь посмотрим на последовательности преобразования, которые необходимы. Для первой перегрузки имеем Host<B,D>::operator D* - одно пользовательское преобразование. Во-вторых, перегрузка сложнее. Нам нужен B *, но возможны две последовательности преобразования. Один через Host<B,D>::operator B*() const. Если (и только если) B и D связаны наследованием, будет существовать последовательность преобразования Host<B,D>::operator D*() + D*->B*. Теперь предположим, что D действительно наследует от B. Две последовательности преобразования: Host<B,D> -> Host<B,D> const -> operator B* const -> B* и Host<B,D> -> operator D* -> D* -> B*.

Таким образом, для связанных B и D, no check(<Host<B,D>(), int()) будет неоднозначным. В результате выбирается шаблон yes check<int>(D*, int). Однако, если D не наследует от B, то no check(<Host<B,D>(), int()) не является двусмысленным. На данный момент разрешение перегрузки не может быть выполнено в кратчайшей последовательности преобразований. Однако, учитывая равные последовательности преобразования, разрешение перегрузки предпочитает функции без шаблона, то есть no check(B*, int).

Теперь вы видите, почему не имеет значения, что наследование является приватным: это отношение служит только для устранения no check(Host<B,D>(), int()) от разрешения перегрузки до проверки доступа. И вы также видите, почему operator B* const должен быть const: иначе нет необходимости в шаге Host<B,D> -> Host<B,D> const, никакой двусмысленности и no check(B*, int) всегда будет выбрано.

Ответ 3

Бит private полностью игнорируется is_base_of, поскольку до проверки доступности происходит перегрузка.

Вы можете проверить это просто:

class Foo
{
public:
  void bar(int);
private:
  void bar(double);
};

int main(int argc, char* argv[])
{
  Foo foo;
  double d = 0.3;
  foo.bar(d);       // Compiler error, cannot access private member function
}

То же самое относится и к тому, что B является частной базой, не мешает проведению проверки, это только предотвращает преобразование, но мы никогда не запрашиваем фактическое преобразование;)

Ответ 4

Возможно, это связано с частичным заказом w.r.t. разрешение перегрузки. D * является более специализированным, чем B *, в случае, когда D происходит от B.

Точные подробности довольно сложны. Вы должны выяснить приоритеты различных правил разрешения перегрузки. Частичное упорядочение - одно. Длины/виды конверсионных последовательностей - еще один. Наконец, если две жизнеспособные функции считаются одинаково хорошими, не-шаблоны выбираются над шаблонами функций.

Мне никогда не приходилось искать, как эти правила взаимодействуют. Но кажется, что частичный порядок доминирует над другими правилами разрешения перегрузки. Когда D не вытекает из B, правила частичного упорядочения не применяются, а не шаблон является более привлекательным. Когда D происходит от B, частичное упорядочение срабатывает и делает шаблон функции более привлекательным - как кажется.

Что касается назначения наследования: код никогда не запрашивает преобразование из D * в B *, что потребует публичного наследования.

Ответ 5

Следуя вашему второму вопросу, обратите внимание, что если бы он не был для const, Host был бы плохо сформирован, если бы он был создан с B == D. Но is_base_of сконструирован таким образом, что каждый класс является базой самого себя, поэтому один из Операторы преобразования должны быть const.