, которая заставила задуматься о том, как вообще можно задетектить закрытое наследование. Лобовое решение, наподобие того, которое применено в boost::is_convertible, приводит только к ошибке компиляции типа: "conversion from 'D *' to 'B *' exists, but is inaccessible". Тогда я решил посмотреть, как реализован boost::is_base_of. После отбрасывания деталей, не касающихся непосредственно вопроса (проверок is_same, is_class, очистка от квалификаторов и пр. др.), образовалась вот такая выжимка (минимальный работающий пример, способный определить наследование, в т.ч. закрытое)
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);
};
//И тестовый пример:class B {};
class D : private B {};
//Успешная компиляция следующей строчки означает, что выражение в квадратных скобках истинноint test[is_base_of<B,D>::value && !is_base_of<D,B>::value];
Я помедитировал над этим, но как это работает не понял. Кто-нибудь может объяснить принцип действия? Просьба только, посмотрите внимательно, пример не такой простой, каким может показаться при беглом взгляде. Обратите внимание на следующие моменты: во-первых, как я уже говорил, D от B может наследоваться закрыто. Во-вторых, в случае, когда наследование имеет место быть, выбирается функция, в парметре которой находится указатель *производного* класса, а не базового, как это интуитивно ожидается. Ну кроме того, такие мелочи как константность операторов неявного преобразования в классе Host, шаблонность/нешаблонность одной из функций check — все имеет значение.
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Я помедитировал над этим, но как это работает не понял.
+3
Аналогично. Надо проводить расследование. Мне лично глаз взрезало "Host<B,D>()" т.к. у операторов данного класса нет зависимостей от "порождаемости" шаблонных параметров — только const. Впрочем, твой пример работает, как заявлено — значит — дыра не в нем, а в моем понимании overload resolution. Бум думать. И еще, хотелось бы и твои думки по тому же поводу увидеть. Со ссылками на стандарт, разумеется. Свои думки — то же самое.
__________
16.There is no cause so right that one cannot find a fool following it.
Здравствуйте, rg45, Вы писали:
R>Я помедитировал над этим, но как это работает не понял. Кто-нибудь может объяснить принцип действия? Просьба только, посмотрите внимательно, пример не такой простой, каким может показаться при беглом взгляде. Обратите внимание на следующие моменты: во-первых, как я уже говорил, D от B может наследоваться закрыто. Во-вторых, в случае, когда наследование имеет место быть, выбирается функция, в парметре которой находится указатель *производного* класса, а не базового, как это интуитивно ожидается. Ну кроме того, такие мелочи как константность операторов неявного преобразования в классе Host, шаблонность/нешаблонность одной из функций check — все имеет значение.
разбираясь с подобными конструкциями, я стараюсь переписать их так чтоб они были доступны в debug-режиме, а именно:
typedef char (&yes)[1];
typedef char (&no)[2];
template <typename B, typename D>
struct Host
{
operator B*() const { return new B; }
operator D*() { return new D; }
};
class C {};
class E : private C {};
template <typename T>
void check(E*, T) { int i = 0; }
void check(C*, int) { int i = 0; }
check(Host<C,E>(), int()); // Host<C,E>::operator E*(Host<C,E>::operator D*) returned E*
check(Host<E,C>(), int()); // Host<E,C>::operator B*(Host<E,C>::operator D*) returned C*const Host<C,E> ConstHostCE;
check(ConstHostCE, int()); // Host<C,E>::operator B*(Host<C,E>::operator B*) returned C*
таким образом, конструкция Host гарантированно возвращает указатель на свой второй параметр. делается это за счет того что функция Host<B,D>::operator B*() имеет модификатор const, а внутри тестирующей функции создается объект без этого модификатора.
Здравствуйте, rg45, Вы писали:
... R>Я помедитировал над этим, но как это работает не понял. Кто-нибудь может объяснить принцип действия?
Я думаю что так:
// Некоторые куски из параграф 13 нарисовал в БНФ (в скобках - сокращения):
implicit conversion sequence (ICS) :=
standard conversion sequence (SCS)
| user-defined conversion sequence (UDCS)
| ellipsis conversion sequence
SCS :=
Identity
| Category_list
Category_list :=
Category Category_opt Category_opt // + правила из 13.3.3.1.1/2
// User-defined conversion sequence Представляет собой(13.3.3.1.2/1)
// последовательность из следующих обязательных частей:
// 1) initial SCS
// 2) user-defined conversion (UDC) // не UDCS!
// 3) second SCS
UDCS :=
SCS UDC SCS
UDC :=
Conversion_function
| ...
Формирование возможных ICS в точке вызова происходит без учета таких параметров как "lifetime, storage class, alignment, or accessibility" (13.3.3.1/2). Поэтому для вызова
check(Host<B,D>(), int())
И в случае если B является базой для D, имеются следующие ICS (для преобразования первого аргумента):
Для функции 'yes check(D*, T)':
UDCS1:
1) H -> H [SCS]{Identity}
2) H -> D* [UDC]
3) D* -> D* [SCS]{Identity}
Для 'no check(B*, int)' есть две "возможные" :
UDCS2.1:
1) H -> H [SCS]{Identity}
1) H -> D* [UDC]
2) D* -> B* [SCS]{Pointer conversion}
UDCS2.2
1) H -> H const [SCS]{Qualification Adjustment}
2) H const -> B* [UDC]
3) B* -> B* [SCS]{Identity}
Какая "настоящая и единственная" conversion sequence из UDCS2.x будет выбрана для 'no check(B*, int)' определяется с помощью 13.3.3.1/6/4:
When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression.
В результате выбирается UDCS2.1. Дальше вступает в силу "partial ordering of implicit conversion sequences" (13.3.3.2) и из UDCS1 и UDCS2.1 выбирается UDCS1 в соответствии с 13.3.3.2/3:
User-defined conversion sequence U1 is a better conversion sequence than another user-defined conversion sequence U2 if they contain the same user-defined conversion function or constructor and if the second standard conversion sequence of U1 is better than the second standard conversion sequence of U2.
Соответственно в результате будет выбрана UDCS1, но поскольку в этой последовательности преобразований нет преобразования D* -> B*, то никаких проверок доступа производится не будет.
Теперь другой случай: B не является базой для D.
Для функции 'yes check(D*, T)':
UDCS1:
1) H -> H [SCS]{Identity}
2) H -> D* [UDC]
3) D* -> D* [SCS]{Identity}
Для 'no check(B*, int)':
UDCS2
1) H -> H const [SCS]{Qualification Adjustment}
2) H const -> B* [UDC]
3) B* -> B* [SCS]{Identity}
Ни одна из них не является лучше другой. 13.3.3.2/3 здесь не применим, т.к. операторы преобразования разные. И если бы 'yes check(D*, T)' не была шаблонной функцией, то возникла бы неоднозначность. Это не так — поэтому 'no check(B*, int)' отдается предпочтение.
Здравствуйте, Юрий Жмеренецкий, Вы писали:
R>>Я помедитировал над этим, но как это работает не понял. Кто-нибудь может объяснить принцип действия? ЮЖ>Я думаю что так: ЮЖ>...
Спасибо, за подробное и толковое разъяснение
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, 0xDEADBEEF, Вы писали:
DEA>И еще, хотелось бы и твои думки по тому же поводу увидеть. Со ссылками на стандарт, разумеется. Свои думки — то же самое.
У меня мозги уже, было, начали закипать, спасибо Юрию Жмеренецкому — спас от выкипания
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>Для функции 'yes check(D*, T)':
UDCS1:
ЮЖ>1) H -> H [SCS]{Identity}
ЮЖ>2) H -> D* [UDC]
ЮЖ>3) D* -> D* [SCS]{Identity}
ЮЖ>Для 'no check(B*, int)' есть две "возможные":
UDCS2.1:
ЮЖ>1) H -> H [SCS]{Identity}
ЮЖ>1) H -> D* [UDC]
ЮЖ>2) D* -> B* [SCS]{Pointer conversion}
ЮЖ>UDCS2.2
ЮЖ>1) H -> H const [SCS]{Qualification Adjustment}
ЮЖ>2) H const -> B* [UDC]
ЮЖ>3) B* -> B* [SCS]{Identity}
И, все-таки, остается ощущение какой-то неудовлетворенности. С точки зрения "простой человеческой логики": если A лучше B и B лучше C, то A лучше С. В данном же случае получается не так. Имеется три UDCS: UDCS1, UDCS2.1 и UDCS2.2. В случае, когда B является базой D имеем: UDCS1 лучше UDCS2.1 и UDCS2.1 лучше UDCS2.2. А что лучше UDCS1 или UDCS2.2? На этот вопрос мы получаем ответ, когда B НЕ является базой D. В этом случае остаются как раз эти два варианта: UDCS1 и UDCS2.2. При этом оказывается, что UDCS2.2 лучше UDCS1. Невольно напрашивается вопрос: почему бы UDCS2.2 не выиграть и в первом случае?
--
Справедливость выше закона. А человечность выше справедливости.
Здравствуйте, rg45, Вы писали:
R>Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>>Для функции 'yes check(D*, T)':
UDCS1:
ЮЖ>>1) H -> H [SCS]{Identity}
ЮЖ>>2) H -> D* [UDC]
ЮЖ>>3) D* -> D* [SCS]{Identity}
ЮЖ>>Для 'no check(B*, int)' есть две "возможные":
UDCS2.1:
ЮЖ>>1) H -> H [SCS]{Identity}
ЮЖ>>1) H -> D* [UDC]
ЮЖ>>2) D* -> B* [SCS]{Pointer conversion}
ЮЖ>>UDCS2.2
ЮЖ>>1) H -> H const [SCS]{Qualification Adjustment}
ЮЖ>>2) H const -> B* [UDC]
ЮЖ>>3) B* -> B* [SCS]{Identity}
R>И, все-таки, остается ощущение какой-то неудовлетворенности. С точки зрения "простой человеческой логики": если A лучше B и B лучше C, то A лучше С. R>В данном же случае получается не так. Имеется три UDCS: UDCS1, UDCS2.1 и UDCS2.2.
В момент когда UDCS1 уже сформирована и ей необходим "партнер" для сравнения, набора из UDCS2.1 и UDCS2.2 уже нет. Выбор между ними происходит раньше. Вот здесь:
When the parameter type is not a reference, the implicit conversion sequence models a copy-initialization of the parameter from the argument expression (13.3.3.1/6).
запускается вложенный механизм overload resolution для выбора user defined converion function. Second SCS в этом контексте не существует — что вполне логично (см. ограничение 13.3.3.1/4). Поэтому в разрешении принимают участие только initial SCS из UDCS2.x: 'H -> H const' vs 'H -> H'.
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>1) H -> H const [SCS]{Qualification Adjustment}
В целом Вы привели хорошее описание, но есть одна маленькая неточность Qualification Adjustment относится только к преобразованию над указателем. В данном случае H -> H const относится к Identity conversion (No conversions required). Это следует из пункта 13.3.3.1.4:
When a parameter of reference type binds directly (8.5.3) to an argument expression, the implicit conversion sequence is the identity conversion, unless the argument expression has a type that is a derived class of the parameter type, in which case the implicit conversion sequence is a derived-to-base Conversion [Example ...] If the parameter binds directly to the result of applying a conversion function to the argument expression, the implicit conversion sequence is a user-defined conversion sequence (13.3.3.1.2), with the second standard conversion sequence either an identity conversion or, if the conversion function returns an entity of a type that is a derived class of the parameter type, a derived-to-base Conversion.
В качестве параметра ссылочного типа здесь выступает implicit object parameter.
Я знаю, что в описании автора данной реализации (Rani Sharoni) присутствует именно Qualification Adjustment и в принципе такая подмена не влияет на ход разрешения перегрузки, но точность всё же не помешает Кстати, я тоже когда-то описывал работу этой реализации: Распознавание наследования на этапе компиляции способом Rani Sharoni
Здравствуйте, Masterkent, Вы писали:
M>Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>>1) H -> H const [SCS]{Qualification Adjustment}
M>В целом Вы привели хорошее описание, но есть одна маленькая неточность Qualification Adjustment относится только к преобразованию над указателем. В данном случае H -> H const относится к Identity conversion (No conversions required).
Действительно, перепутал с "reference-compatible with added qualification"
M>Это следует из пункта 13.3.3.1.4: M>
When a parameter of reference type binds directly (8.5.3) to an argument expression, the implicit conversion sequence is the identity conversion, unless the argument expression has a type that is a derived class of the parameter type, in which case the implicit conversion sequence is a derived-to-base Conversion [Example ...] If the parameter binds directly to the result of applying a conversion function to the argument expression, the implicit conversion sequence is a user-defined conversion sequence (13.3.3.1.2), with the second standard conversion sequence either an identity conversion or, if the conversion function returns an entity of a type that is a derived class of the parameter type, a derived-to-base Conversion.
M>В качестве параметра ссылочного типа здесь выступает implicit object parameter.
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>в обсуждаемом примере связывание ссылок отсутствует.
Почему же? В стандарте есть следующие положения:
13.3.1/2
The set of candidate functions can contain both member and non-member functions to be resolved against the same argument list. So that argument and parameter lists are comparable within this heterogeneous set, a member function is considered to have an extra parameter, called the implicit object parameter, which represents the object for which the member function has been called. For the purposes of overload resolution, both static and non-static member functions have an implicit object parameter, but constructors do not.
13.3.1/4
For non-static member functions, the type of the implicit object parameter is “reference to cv X” where X is the class of which the function is a member and cv is the cv-qualification on the member function declaration. [Example: for a const member function of class X, the extra parameter is assumed to have type “reference to const X”. ] For conversion functions, the function is considered to be a member of the class of the implicit object argument for the purpose of defining the type of the implicit object parameter.
В случае, когда D прямо или косвенно унаследован от B, для вычисления первого аргумента функции no check(B*, int) мы имеем две viable функции:
Host::operator B*() const с неявным объектным параметром типа Host const& и Host::operator D*() с неявным объектным параметром типа Host&
Перегрузка в отношении этих двух функций осуществляется по следующим правилам:
13.3.3.2/3/1
— Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence
S2 if
[...]
— S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers.
где под S1 следует подставить связывание параметра типа Host& с аргументом Host<B, D>(), а под S2 — связывание параметра типа Host const& с таким же аргументом;
13.3.3/1
a viable function F1 is defined to be a better function than another viable function F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then — for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2)
где под F1 следует подставить Host::operator D*(), под F2 — Host::operator B*() const, под ICS1(F1) — связывание параметра типа Host& с аргументом Host<B, D>(), под ICS1(F2) — связывание параметра типа Host const& с таким же аргументом.
Связывание неконстантной ссылки с rvalue в данном случае допустимо ввиду
13.3.1/5
The implicit object parameter, however, retains its identity since conversions on the corresponding argument shall obey these additional rules:
— no temporary object can be introduced to hold the argument for the implicit object parameter;
— no user-defined conversions can be applied to achieve a type match with it; and
— even if the implicit object parameter is not const-qualified, an rvalue temporary can be bound to the parameter as long as in all other respects the temporary can be converted to the type of the implicit object parameter.
Здравствуйте, Masterkent, Вы писали:
M>Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>>Здесь применяется 13.3.3.1/6, т.к. в обсуждаемом примере связывание ссылок отсутствует.
M>Почему же?:
Мы говорим о разных вещах: все сказанное не относилось к 'implicit object parameter', речь шла только о первом параметре функции 'check'.
M>Поэтому в случае, когда D прямо или косвенно унаследован от B, для вычисления первого аргумента функции is_base_of<B, D>::no check(B*, int) мы имеем две viable функции:
M>Host::operator B*() const с неявным объектным параметром типа Host const& и M>Host::operator D*() с неявным объектным параметром типа Host&
Здесь возникает вопрос — откуда появились эти две функции в списке кандидатов?
Два этих варианта появляются в результате моделирования процесса copy-initialization, который является частью вложенного механизма overload resolution. Первоначально, для того что бы функция 'no check(B*, int)' попала в список 'viable' функций (для check(Host<...)) необходимо наличие преобразования 'Host<B,D>' -> 'B*'. Для определения конкретных шагов преобразования применяется 13.3.3.1/6.
Здравствуйте, Юрий Жмеренецкий, Вы писали:
ЮЖ>Мы говорим о разных вещах: все сказанное не относилось к 'implicit object parameter', речь шла только о первом параметре функции 'check'.
В моём случае речь шла о том, чем является и чем не является последовательность преобразований, применяемая для аргумента Host<B, D>(), передаваемого в функцию Host<B, D>::operator B*() const. Это reference binding, где нет каких-либо Qualification conversions, и, соответственно, для определения лучшей последовательности преобразований при разрешении перегрузки с целью выбора лучшей функции преобразования (от этого выбора зависит, какая из последовательностей — UDCS2.1 или UDCS2.2 (в Ваших обозначениях) — должна быть использована) вместо 13.3.3.2/3/1/3 будет применено 13.3.3.2/3/1/4.
Здравствуйте, rg45, Вы писали:
ЮЖ>>Для функции 'yes check(D*, T)':
UDCS1:
ЮЖ>>1) H -> H [SCS]{Identity}
ЮЖ>>2) H -> D* [UDC]
ЮЖ>>3) D* -> D* [SCS]{Identity}
ЮЖ>>Для 'no check(B*, int)' есть две "возможные":[ccode]UDCS2.1: ЮЖ>>1) H -> H [SCS]{Identity} ЮЖ>>1) H -> D* [UDC] ЮЖ>>2) D* -> B* [SCS]{Pointer conversion}
R>И, все-таки, остается ощущение какой-то неудовлетворенности. С точки зрения "простой человеческой логики": если A лучше B и B лучше C, то A лучше С. В данном же случае получается не так. Имеется три UDCS: UDCS1, UDCS2.1 и UDCS2.2. В случае, когда B является базой D имеем: UDCS1 лучше UDCS2.1 и UDCS2.1 лучше UDCS2.2. А что лучше UDCS1 или UDCS2.2? На этот вопрос мы получаем ответ, когда B НЕ является базой D. В этом случае остаются как раз эти два варианта: UDCS1 и UDCS2.2. При этом оказывается, что UDCS2.2 лучше UDCS1. Невольно напрашивается вопрос: почему бы UDCS2.2 не выиграть и в первом случае?
В общем, для меня это тоже сначала осталось непонятым. Вот, что прояснилось, почти обо всем уже упомянули, чтобы прояснить картину осталось только собрать все воедино:
При выборе между UDCS2.1 и UDCS2.2, т.е. при выборе пользовательского преобразования H -> B*, происходит вложенное разрешение перегрузки для выбора функции преобразования:
13.3.1.5/1 Initialization by conversion function
Under the conditions specified in 8.5, as part of an initialization of an object of nonclass type, a conversion
function can be invoked to convert an initializer expression of class type to the type of the object being initialized. Overload resolution is used to select the conversion function to be invoked. Assuming that “cv1
T” is the type of the object being initialized, and “cv S” is the type of the initializer expression, with S a
class type, the candidate functions are selected as follows:
Под инициализацией здесь понимается также "argument passing" (8.5/12).
При разрешении перегрузки принимается во внимание только один аргумент, здесь же в 13.3.1.5:
13.3.1.5/2
The argument list has one argument, which is the initializer expression. [Note: this argument will be compared
against the implicit object parameter of the conversion functions. ]
Т.е. выбор происходит между преобразованиями к типу "implicit object parameter" H -> H& и H -> H const&. Причем, как указал Masterkent биндинг rvalue к неконстантной ссылке в данном случае допустим (13.3.1/5). Очевидно, что в данном случае выбор происходит в пользу H -> H& в соответствии с 13.3.3.2/3.
В том случае, если невозможно было бы выбрать лучшую последовательность преобразований, например, если бы оба оператора преобразования Н являлись неконстантными членами, действовало бы следующее правило:
Given these definitions, a viable function F1 is defined to be a better function than another viable function
F2 if for all arguments i, ICSi(F1) is not a worse conversion sequence than ICSi(F2), and then
— for some argument j, ICSj(F1) is a better conversion sequence than ICSj(F2), or, if not that,
... — the context is an initialization by user-defined conversion (see 8.5, 13.3.1.5, and 13.3.1.6) and the standard
conversion sequence from the return type of F1 to the destination type (i.e., the type of the entity
being initialized) is a better conversion sequence than the standard conversion sequence from the return
type of F2 to the destination type. [Example:
struct A {
A();
operator int();
operator double();
} a;
int i = a; // a.operator int() followed by no conversion
// is better than a.operator double() followed by
// a conversion to int
float x = a; // ambiguous: both possibilities require conversions,
// and neither is better than the other
—end example]
Т.е. в таком случае при выборе имело бы значение стандартное преобразование типа возращаемого функцией преобразования к destination type, в нашем случае к типу B* (D* -> B* или B* -> B*). В рассматриваемом разрешении перегрузки данное правило не реализуется.