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

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

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

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


person Alexey Malistov    schedule 26.05.2010    source источник
comment
Вас очень смущает использование одного и того же идентификатора для параметра шаблона и истинного имени класса...   -  person Matthieu M.    schedule 26.05.2010
comment
@Matthieu M., я взял на себя ответственность исправить :)   -  person Kirill V. Lyadvinsky    schedule 26.05.2010
comment
Вы имели в виду в 3: Почему static yes check<int>(D*, T=int); лучше, чем static no check(B*, int);?   -  person MSalters    schedule 26.05.2010
comment
@MSalters. Нет. Почему шаблонная функция лучше, чем нешаблонная. И почему важно, чтобы operator B*() было константой?   -  person Alexey Malistov    schedule 26.05.2010
comment
Некоторое время назад я написал альтернативную реализацию is_base_of: ideone.com/T0C1V Она не работает со старыми версиями GCC. версии, хотя (GCC4.3 работает нормально).   -  person Johannes Schaub - litb    schedule 26.05.2010
comment
@Alexey: вы спрашивали о static yes check(B*, int);, но в коде есть только static no check(B*, int);.   -  person MSalters    schedule 26.05.2010
comment
@litb, ваше альтернативное решение на ideone.com не работает на VC2008 и VC2010.   -  person Kirill V. Lyadvinsky    schedule 27.05.2010
comment
@Kirill, вероятно, это потому, что он позволяет привязывать неконстантные ссылки к значениям rvalue. Если отключить расширения, то может и там работает. В любом случае, он должен предупреждать на высоких уровнях предупреждения.   -  person Johannes Schaub - litb    schedule 28.05.2010
comment
Хорошо, я пойду прогуляюсь.   -  person jokoon    schedule 04.01.2011
comment
@JohannesSchaub-litb: ссылка на ideone.com/T0C1V приводит к пустой странице   -  person slashmais    schedule 10.01.2015
comment
@MatthieuM. Вот почему я всегда ставлю перед template аргументами префикс T_ ;-)   -  person underscore_d    schedule 27.02.2016
comment
В gcc is_base_of является производным от __is_base_of. Я нигде не могу найти определение __is_base_of . Кто-нибудь знает, в каком заголовке определен __is_base_of?   -  person QuentinUK    schedule 22.05.2016
comment
Эта реализация неверна. is_base_of<Base,Base>::value должно быть true; это возвращает false.   -  person ST0    schedule 01.06.2016
comment
@QuentinUK: __is_base_of(T,U) — это встроенный компилятор, также известный как магия. Это не часть языка C++; это присуще GCC. Единственная причина, по которой он существует (насколько мне известно), состоит в том, чтобы ускорить оценку этого свойства типа, что (если вы не используете встроенную функцию magic) требует большого количества экземпляров шаблона и разрешения перегрузки. Таких магических встроенных функций гораздо больше, например __is_union, __is_trivially_constructible и __is_final.   -  person Quuxplusone    schedule 20.05.2017
comment
@chengiz: можно ли исправить эту реализацию, чтобы исправить эту ошибку?   -  person Alexandr Zarubkin    schedule 17.06.2017


Ответы (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.

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

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 нет наследования!

person Johannes Schaub - litb    schedule 26.05.2010
comment
Ах! У Андреаса был правильный абзац, жаль, что он не дал такого ответа :) Спасибо за ваше время, я хотел бы поставить его в избранное. - person Matthieu M.; 27.05.2010
comment
Это будет мой любимый ответ... вопрос: вы прочитали весь стандарт C++ или вы просто работаете в комитете C++?? Поздравляем! - person Marco A.; 02.02.2014
comment
@DavidKernin, работающий в комитете C++, автоматически не дает вам знать, как работает C++ :) Так что вам определенно нужно прочитать ту часть Стандарта, которая необходима, чтобы узнать подробности, что я и сделал. Не читал все это, поэтому я определенно не могу помочь с большей частью стандартной библиотеки или вопросами, связанными с потоками :) - person Johannes Schaub - litb; 02.02.2014
comment
Фантастическое объяснение. Но это заставляет меня задаться вопросом: в какой момент комитет согласится с тем, что определенные проверки во время компиляции лучше реализовать в виде ключевых слов, чем в виде — умной, но весьма замысловатой — шаблонной акробатики? - person underscore_d; 27.02.2016
comment
@underscore_d Честно говоря, спецификация не запрещает свойствам std:: использовать некоторую магию компилятора, поэтому разработчики стандартной библиотеки могут использовать их по своему усмотрению. Они избегают акробатики шаблонов, что также помогает ускорить время компиляции и использование памяти. Это верно, даже если интерфейс выглядит как std::is_base_of<...>. Это все под капотом. - person Johannes Schaub - litb; 28.02.2016
comment
Конечно, общие библиотеки, такие как boost::, должны убедиться, что у них есть эти встроенные функции, прежде чем использовать их. И у меня есть ощущение, что среди них есть какой-то вызов, принятый менталитетом, чтобы реализовать вещи без помощи компилятора :) - person Johannes Schaub - litb; 28.02.2016
comment
@JohannesSchaub-litb Хорошо, я думаю, что встроенные функции компилятора ближе всего к тому, что я предполагал, насколько язык сопротивляется добавлению новых ключевых слов! TMP определенно может быть проблемой и даже развлечением... до определенного момента :D - person underscore_d; 28.02.2016

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

Начните с части 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).

person MSalters    schedule 26.05.2010
comment
Ваше объяснение не учитывает присутствие const. Если ваш ответ верен, то const не требуется. Но это неправда. Удалите const и трюк не сработает. - person Alexey Malistov; 26.05.2010
comment
Без константы две последовательности преобразования для no check(B*, int) больше не являются двусмысленными. - person MSalters; 26.05.2010
comment
Если оставить только no check(B*, int), то для связанных B и D неоднозначности не будет. Компилятор однозначно выбрал бы operator D*() для выполнения преобразования, потому что у него нет константы. Это скорее немного в противоположном направлении: если вы удаляете константу, вы вводите некоторое ощущение двусмысленности, но это разрешается тем фактом, что operator B*() обеспечивает превосходный возвращаемый тип, который не нуждается в указателе. преобразование в B*, как это делает D*. - person Johannes Schaub - litb; 26.05.2010
comment
В том-то и дело: двусмысленность возникает между двумя разными последовательностями преобразования, чтобы получить B* из временного <Host<B,D>(). - person MSalters; 27.05.2010
comment
Это лучший ответ. Спасибо! То есть, как я понял, если одна функция лучше, но неоднозначна, то выбирается другая функция? - person user1289; 07.07.2015
comment
@GrigorApoyan: Действительно. Но обратите внимание, что это не общее правило. С шаблонами ошибка замены не является ошибкой (SFINAE). В начале разработки C++ было замечено, что непреднамеренное создание экземпляров шаблона часто было бы ошибкой, поэтому было введено правило SFINAE. Это правило теперь часто намеренно используется в подобных ситуациях. - person MSalters; 07.07.2015

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

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

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

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

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

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

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

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

person Hertz    schedule 10.03.2014