Как работает `is_base_of`? - PullRequest
114 голосов
/ 26 мая 2010

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

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];
  1. Обратите внимание, что B является частной базой. Как это работает?

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

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

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

Ответы [ 5 ]

107 голосов
/ 26 мая 2010

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

Давайте на минутку предположим, что 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>&)

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

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

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

Это будет означать, что мы больше не можем выбирать по постоянству. В обычном сценарии разрешения перегрузки вызов теперь будет неоднозначным, поскольку обычно возвращаемый тип не участвует в разрешении перегрузки. Для функций преобразования, однако, есть черный ход. Если две функции преобразования одинаково хороши, то тип возвращаемого значения определяет, кто лучше, согласно 13.3.3/1. Таким образом, если вы удалите const, то будет взято первое, потому что 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!

нет наследования
24 голосов
/ 26 мая 2010

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

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

4 голосов
/ 26 мая 2010

Бит 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 является частной базой, не препятствует выполнению проверки, она только помешает конвертации, но мы никогда не запрашиваем фактическую конверсию;)

2 голосов
/ 26 мая 2010

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

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

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

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

0 голосов
/ 10 марта 2014

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

...