Як працює `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
Вам дуже заплутано використовувати той самий ідентифікатор для параметра шаблону та справжнього імені класу ...
Matthieu M.

1
@Matthieu M., я взяв на себе виправити :)
Лядвінський Кирило Васильович

2
Деякий час тому я писав альтернативну реалізацію is_base_of: ideone.com/T0C1V Хоча це не працює зі старими версіями GCC (GCC4.3 працює чудово).
Йоханнес Шауб - ліб

3
Гаразд, я йду гуляти.
jokoon

2
Ця реалізація є невірною. is_base_of<Base,Base>::valueповинно бути true; це повертається false.
chengiz

Відповіді:


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>&)

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

Якби видалити Конст, ми мали б такі кандидати

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

Це означатиме, що ми не можемо більше вибирати за допомогою constness. У звичайному сценарії розв’язання перевантаження виклик тепер буде неоднозначним, оскільки зазвичай тип повернення не бере участі у вирішенні перевантаження. Для функцій перетворення, проте, є задній простір. Якщо дві функції перетворення однаково хороші, то тип повернення їх визначає, хто найкраще 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!


2
Ах! Андреас мав абзац правильно, шкода, що він не дав такої відповіді :) Дякую за ваш час, я б хотів, щоб я міг полюбити його.
Матьє М.

2
Це стане моєю улюбленою відповіддю колись ... запитання: чи читали ви цілий стандарт C ++ чи ви просто працюєте в комітеті C ++ ?? Вітаємо!
Марко А.

4
@DavidKernin, що працює у комітеті C ++, автоматично не дає вам знати, як працює C ++ :) Отже, вам обов'язково потрібно прочитати частину стандарту, яка потрібна для того, щоб знати деталі, які я зробив. Я не прочитав все це, тому я точно не можу допомогти з більшою частиною бібліотеки «Стандарт» або з вирізанням пов’язаних питань :)
Йоханнес Шауб - ліб

1
@underscore_d Якщо чесно, специфікація не забороняє характеристикам std :: використовувати деякі магії компілятора, тому стандартні бібліотечні реалізатори можуть використовувати їх так, як їм подобається . Вони уникнуть акробатики шаблону, що також сприяє прискоренню збирання часу та використання пам'яті. Це вірно, навіть якщо інтерфейс виглядає так std::is_base_of<...>. Це все під кришкою.
Йоханнес Шауб - ліб

2
Звичайно, загальні бібліотеки люблять boost::переконатися, що вони мають ці властивості, перш ніж використовувати їх. І я маю відчуття, що серед них є якийсь менталітет "прийнятих викликів", щоб реалізовувати речі без допомоги компілятора :)
Йоханнес Шауб - ліб

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треба констатувати: інакше немає необхідності ні в Host<B,D> -> Host<B,D> constкроці, ні в неоднозначності, і no check(B*, int)їх завжди обирають.


Ваше пояснення не враховує наявність const. Якщо ваша відповідь вірна, тоді жодна constне потрібна. Але це неправда. Видалити constі трюк не вийде.
Олексій Малістов

Без const дві послідовності перетворення для no check(B*, int)більше не є неоднозначними.
MSalters

Якщо ви виїдете лише no check(B*, int), то для споріднених Bі D, це було б неоднозначно. Компілятор однозначно вирішив operator D*()би здійснити конверсію, оскільки не має const. Це скоріше трохи в протилежному напрямку: Якщо ви видалите const, ви введете певне відчуття двозначності, але це вирішується тим, що operator B*()забезпечує вищий тип повернення, який не потребує перетворення вказівника на B*подібний D*.
Йоханнес Шауб - ліб

В цьому і полягає суть: неоднозначність між двома різними послідовностями перетворення, щоб отримати а B* з <Host<B,D>()тимчасового.
MSalters

Це краща відповідь. Дякую! Отже, як я зрозумів, якщо одна функція краща, але неоднозначна, то вибирається інша функція?
user1289

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, безумовно, є більш повною, хоча я з нетерпінням чекаю чіткого пояснення процесу повного вирішення залежно від випадків.
Матьє М.

2

Це, можливо, пов'язане з частковим замовленням роздільної здатності перевантаження. D * є більш спеціалізованим, ніж B * у випадку, коли D походить від B.

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

Мені ніколи не потрібно було шукати, як взаємодіють ці правила. Але, здається, часткове впорядкування домінує над іншими правилами розв'язання перевантаження. Якщо D не походить від B, часткові правила впорядкування не застосовуються, а нешаблон є більш привабливим. Коли D походить від B, часткове впорядкування запускає і робить шаблон функції більш привабливим - як здається.

Що ж стосується приватного спадкування: код ніколи не вимагає перетворення з D * в B *, що вимагало б публічного успадкування.


Я думаю, що це щось подібне, я пам’ятаю, що бачив велику дискусію в архівах збільшених питань щодо впровадження is_base_ofта циклів, які проходили учасники, щоб забезпечити це.
Матьє М.

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 розроблений таким чином, що кожен клас є базою для себе, отже, один з операторів перетворення повинен бути const.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.