Как работает `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
, И это работает на широком спектре компиляторов.
5 ответов
Если они связаны
Давайте на минутку предположим, что 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
!
Давайте разберемся, как это работает, посмотрев на шаги.
Начните с 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
должно быть const: иначе нет необходимости Host<B,D> -> Host<B,D> const
шаг, никакой двусмысленности, и no check(B*, int)
всегда будет выбран.
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
это частная база не препятствует проверке, она только помешает конвертации, но мы никогда не запрашиваем фактическую конверсию;)
Возможно, это как-то связано с частичным упорядочением по разрешению перегрузки. D* более специализирован, чем B* в случае, если D происходит от B.
Точные детали довольно сложны. Вы должны выяснить приоритеты различных правил разрешения перегрузки. Частичный заказ один. Длина / виды последовательностей преобразования является еще одним. Наконец, если две жизнеспособные функции считаются одинаково хорошими, не шаблоны выбираются из шаблонов функций.
Мне никогда не нужно было искать, как эти правила взаимодействуют. Но кажется, что частичное упорядочение доминирует над другими правилами разрешения перегрузки. Когда D не выводится из B, правила частичного упорядочения не применяются, и шаблон не является более привлекательным. Когда D выводится из B, включается частичное упорядочение и делает шаблон функции более привлекательным - как это кажется.
Что касается наследования, являющегося частным: код никогда не запрашивает преобразование из D * в B*, что потребовало бы публичного наследования.
Следуя вашему второму вопросу, обратите внимание, что если бы не const, Host был бы плохо сформирован, если был создан экземпляр с B == D. Но is_base_of разработан так, что каждый класс является основой самого себя, поэтому один из операторов преобразования должен быть постоянным