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

118

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

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*()это const. Почему это важно?

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

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

Алексей Малистов
источник
4
С вашей стороны очень сбивает с толку использование одного и того же идентификатора для параметра шаблона и настоящего имени класса ...
Мэттью М.
1
@Matthieu M., взял на себя
ответственность
2
Некоторое время назад я написал альтернативную реализацию is_base_of: ideone.com/T0C1V Однако она не работает со старыми версиями GCC (GCC4.3 отлично работает).
Йоханнес Шауб - лит
3
Хорошо, я пойду прогуляться.
jokoon 04
2
Эта реализация неверна. is_base_of<Base,Base>::valueдолжно быть true; это возвращается false.
chengiz 01

Ответы:

109

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

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

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

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* . Таким образом выбирается первая функция и мы распознаем наследование!

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

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

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

D* (Host<B, D>&) 

И на секунду у нас теперь есть еще один набор

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

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

Йоханнес Шауб - litb
источник
2
Ах! Андреас правильно написал абзац, жаль, что он не дал такого ответа :) Спасибо за ваше время, я бы хотел поставить его в избранное.
Matthieu M.
2
Это будет мой любимый ответ на все времена ... вопрос: вы читали весь стандарт C ++ или вы просто работаете в комитете C ++ ?? Поздравляем!
Марко А.
4
@DavidKernin, работающий в коммите C ++, не дает вам автоматически знать, как работает C ++ :) Так что вам определенно нужно прочитать ту часть Стандарта, которая необходима, чтобы узнать подробности, что я и сделал. Я еще не читал все это, поэтому я определенно не могу помочь с большей частью стандартной библиотеки или с вопросами, связанными с
потоками
1
@underscore_d Честно говоря, спецификация не запрещает std :: traits использовать некоторую магию компилятора, чтобы разработчики стандартной библиотеки могли использовать их по своему усмотрению . Они избегают акробатических приемов шаблонов, что также помогает ускорить время компиляции и использование памяти. Это верно, даже если интерфейс выглядит так std::is_base_of<...>. Все под капотом.
Йоханнес Шауб - лит
2
Конечно, общие библиотеки, как и boost::прежде, должны убедиться, что у них есть эти встроенные функции, прежде чем использовать их. И у меня такое чувство, что среди них есть какой-то менталитет «принять вызов», чтобы реализовать что-то без помощи компилятора :)
Йоханнес Шауб - litb
24

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

Начнем с 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)всегда будет выбран.

MSalters
источник
В вашем объяснении не учитывается наличие const. Если ваш ответ верный, то в этом нет constнеобходимости. Но это не так. Удалить constи обмануть не получится.
Алексей Малистов
Без const две последовательности преобразования для no check(B*, int)больше не являются неоднозначными.
MSalters
Если оставить только no check(B*, int), то для родственных Bи Dнеоднозначно не будет. Компилятор однозначно выберет operator D*()выполнение преобразования, потому что у него нет константы. Это скорее немного в противоположном направлении: если вы удалите константу, вы введете некоторую двусмысленность, но она разрешается тем фактом, что operator B*()предоставляет превосходный возвращаемый тип, который не требует преобразования указателя в B*подобные D*.
Йоханнес Шауб - лит
Это действительно суть: двусмысленность существует между двумя разными последовательностями преобразования, чтобы получить a B*из <Host<B,D>()временного.
MSalters
Это лучший ответ. Спасибо! Итак, как я понял, если одна функция лучше, но неоднозначна, то выбирается другая?
user1289 07
4

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

Матье М.
источник
Вроде, как бы, что-то вроде. Базовое преобразование вообще не выполняется. hostпроизвольно преобразуется в невычисленное выражение D*или B*в него. По некоторым причинам при определенных условиях D*предпочтительнее B*.
Potatoswatter
Я думаю, что ответ в 13.3.1.1.2, но я еще не разобрался в деталях :)
Андреас Бринк,
Мой ответ объясняет только часть «почему даже частные работы», ответ sellibitze, безусловно, более полный, хотя я с нетерпением жду четкого объяснения процесса полного разрешения в зависимости от случая.
Matthieu M.
2

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

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

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

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

sellibitze
источник
Я думаю, что это что-то в этом роде, я помню, как видел обширное обсуждение архивов повышения, реализации is_base_ofи циклов, через которые участники прошли, чтобы гарантировать это.
Matthieu M.
The exact details are rather complicated- в этом-то и дело. Пожалуйста, объясни. Я действительно хочу знать.
Алексей Малистов
@ Алексей: Ну, я думал, что указал тебе правильное направление. Посмотрите, как в этом случае взаимодействуют различные правила разрешения перегрузки. Единственное различие между D, полученным из B, и D, не полученным из B, в отношении разрешения этого случая перегрузки - это правило частичного упорядочивания. Разрешение перегрузки описано в §13 стандарта C ++. Вы можете получить черновик бесплатно: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
Разрешение перегрузки в этом черновике составляет 16 страниц. Думаю, если вам действительно нужно понять правила и взаимодействие между ними в этом случае, вам следует полностью прочитать раздел §13.3. Я бы не стал рассчитывать на получение здесь 100% правильного и соответствующего вашим стандартам ответа.
sellibitze
пожалуйста, посмотрите мой ответ для объяснения этого, если вам интересно.
Йоханнес Шауб - лит
0

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

Герц
источник