Comment fonctionne le code suivant?
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];
Notez que B
est une base privée. Comment cela marche-t-il?
Notez que operator B*()
est const. Pourquoi c'est important?
Pourquoi template<typename T> static yes check(D*, T);
est-il meilleur que static yes check(B*, int);
?
Remarque: Il s'agit d'une version réduite (les macros sont supprimées) de boost::is_base_of
. Et cela fonctionne sur une large gamme de compilateurs.
Supposons un instant que B
soit en fait une base de D
. Ensuite, pour l'appel à check
, les deux versions sont viables car Host
peut être converti en D*
et B*
. Il s'agit d'une séquence de conversion définie par l'utilisateur, décrite par 13.3.3.1.2
De Host<B, D>
À D*
Et B*
Respectivement. Pour trouver des fonctions de conversion capables de convertir la classe, les fonctions candidates suivantes sont synthétisées pour la première fonction check
selon 13.3.1.5/1
D* (Host<B, D>&)
La première fonction de conversion n'est pas candidate, car B*
Ne peut pas être converti en D*
.
Pour la deuxième fonction, les candidats suivants existent:
B* (Host<B, D> const&)
D* (Host<B, D>&)
Ce sont les deux candidats à la fonction de conversion qui prennent l'objet Host. Le premier le prend par référence const, et le second non. Ainsi, le second correspond mieux à l'objet non const *this
(L'argument implicite de l'objet ) par 13.3.3.2/3b1sb4
et est utilisé pour convertir en B*
pour la deuxième fonction check
.
Si vous voulez supprimer la const, nous aurions les candidats suivants
B* (Host<B, D>&)
D* (Host<B, D>&)
Cela signifierait que nous ne pouvons plus sélectionner par constance. Dans un scénario de résolution de surcharge ordinaire, l'appel serait désormais ambigu car normalement le type de retour ne participera pas à la résolution de surcharge. Pour les fonctions de conversion, cependant, il existe une porte dérobée. Si deux fonctions de conversion sont également bonnes, leur type de retour décide qui est le meilleur selon 13.3.3/1
. Ainsi, si vous supprimez la constante, la première serait prise, car B*
Se convertit mieux en B*
Que D*
En B*
.
Quelle est la meilleure séquence de conversion définie par l'utilisateur? Celui pour la deuxième ou la première fonction de vérification? La règle est que les séquences de conversion définies par l'utilisateur ne peuvent être comparées que si elles utilisent la même fonction de conversion ou le même constructeur selon 13.3.3.2/3b2
. C'est exactement le cas ici: les deux utilisent la deuxième fonction de conversion. Notez que donc const est important car il oblige le compilateur à prendre la deuxième fonction de conversion.
Puisque nous pouvons les comparer - lequel est le meilleur? La règle est que la meilleure conversion du type de retour de la fonction de conversion au type de destination gagne (encore une fois par 13.3.3.2/3b2
). Dans ce cas, D*
Convertit mieux en D*
Qu'en B*
. Ainsi, la première fonction est sélectionnée et nous reconnaissons l'héritage!
Notez que puisque nous n'avons jamais eu besoin de réellement convertir en une classe de base, nous pouvons ainsi reconnaître héritage privé parce que si nous pouvons convertir d'un D*
à un B*
ne dépend pas de la forme d'héritage selon 4.10/3
Supposons maintenant qu'ils ne sont pas liés par héritage. Ainsi, pour la première fonction, nous avons les candidats suivants
D* (Host<B, D>&)
Et pour la seconde, nous avons maintenant un autre ensemble
B* (Host<B, D> const&)
Puisque nous ne pouvons pas convertir D*
En B*
Si nous n'avons pas de relation d'héritage, nous n'avons maintenant aucune fonction de conversion commune parmi les deux séquences de conversion définies par l'utilisateur! Ainsi, nous serions ambigus sinon pour le fait que la première fonction est un modèle. Les modèles sont le deuxième choix lorsqu'il existe une fonction non-modèle qui est également bonne selon 13.3.3/1
. Ainsi, nous sélectionnons la fonction non modèle (deuxième) et nous reconnaissons qu'il n'y a pas d'héritage entre B
et D
!
Voyons comment cela fonctionne en regardant les étapes.
Commencez par la partie sizeof(check(Host<B,D>(), int()))
. Le compilateur peut rapidement voir que cette check(...)
est une expression d'appel de fonction, elle doit donc faire une résolution de surcharge sur check
. Il existe deux surcharges candidates, template <typename T> yes check(D*, T);
et no check(B*, int);
. Si le premier est choisi, vous obtenez sizeof(yes)
, sinon sizeof(no)
Ensuite, regardons la résolution de surcharge. La première surcharge est une instanciation de modèle check<int> (D*, T=int)
et le deuxième candidat est check(B*, int)
. Les arguments réels fournis sont Host<B,D>
Et int()
. Le deuxième paramètre ne les distingue clairement pas; il servait simplement à faire de la première surcharge un modèle. Nous verrons plus tard pourquoi la partie modèle est pertinente.
Regardez maintenant les séquences de conversion nécessaires. Pour la première surcharge, nous avons Host<B,D>::operator D*
- une conversion définie par l'utilisateur. Pour le second, la surcharge est plus délicate. Nous avons besoin d'un B *, mais il y a peut-être deux séquences de conversion. L'une est via Host<B,D>::operator B*() const
. Si (et seulement si) B et D sont liés par héritage, la séquence de conversion Host<B,D>::operator D*()
+ D*->B*
Existera. Supposons maintenant que D hérite en effet de B. Les deux séquences de conversion sont Host<B,D> -> Host<B,D> const -> operator B* const -> B*
Et Host<B,D> -> operator D* -> D* -> B*
.
Ainsi, pour B et D connexes, no check(<Host<B,D>(), int())
serait ambigu. En conséquence, la yes check<int>(D*, int)
modèle est choisie. Cependant, si D n'hérite pas de B, alors no check(<Host<B,D>(), int())
n'est pas ambigu. À ce stade, la résolution de surcharge ne peut pas se produire sur la base de la séquence de conversion la plus courte. Cependant, à égalité de séquences de conversion, la résolution de surcharge préfère les fonctions non-modèle, c'est-à-dire no check(B*, int)
.
Vous voyez maintenant pourquoi il n'a pas d'importance que l'héritage soit privé: cette relation ne sert qu'à éliminer no check(Host<B,D>(), int())
de la résolution de surcharge avant que le contrôle d'accès ne se produise. Et vous voyez aussi pourquoi le operator B* const
Doit être const: sinon il n'y a pas besoin de l'étape Host<B,D> -> Host<B,D> const
, Aucune ambiguïté, et no check(B*, int)
serait toujours choisi.
Le bit private
est complètement ignoré par is_base_of
car la résolution de surcharge se produit avant les vérifications d'accessibilité.
Vous pouvez le vérifier simplement:
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
}
La même chose s'applique ici, le fait que B
est une base privée n'empêche pas la vérification d'avoir lieu, cela empêcherait seulement la conversion, mais nous ne demandons jamais la conversion réelle;)
Cela a peut-être quelque chose à voir avec la commande partielle sans t. résolution de surcharge. D * est plus spécialisé que B * dans le cas où D dérive de B.
Les détails exacts sont plutôt compliqués. Vous devez déterminer les priorités de diverses règles de résolution de surcharge. La commande partielle en est un. Les longueurs/types de séquences de conversion en sont une autre. Enfin, si deux fonctions viables sont jugées également bonnes, les non-modèles sont choisis parmi les modèles de fonction.
Je n'ai jamais eu besoin de chercher comment ces règles interagissent. Mais il semble que l'ordre partiel domine les autres règles de résolution de surcharge. Lorsque D ne dérive pas de B, les règles de classement partiel ne s'appliquent pas et le non-modèle est plus attrayant. Lorsque D dérive de B, l'ordre partiel entre en jeu et rend le modèle de fonction plus attrayant - comme il semble.
Quant à l'héritage étant privé: le code ne demande jamais une conversion de D * en B * qui nécessiterait l'héritage public.
Suite à votre deuxième question, notez que si ce n'était pas pour const, Host serait mal formé s'il était instancié avec B == D. Mais is_base_of est conçu de telle sorte que chaque classe est une base en soi, donc l'un des opérateurs de conversion doit être const.