118 votes

Comment fonctionne `is_base_of` ?

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];
  1. Notez que B est une base privée. Comment cela fonctionne-t-il ?

  2. Notez que operator B*() est constant. Pourquoi est-ce important ?

  3. Pourquoi est-ce que template<typename T> static yes check(D*, T); mieux que static yes check(B*, int); ?

Note : C'est une version réduite (les macros sont supprimées) de boost::is_base_of . Et cela fonctionne sur une large gamme de compilateurs.

4 votes

C'est très confus de votre part d'utiliser le même identifiant pour un paramètre de modèle et un vrai nom de classe...

1 votes

@Matthieu M., j'ai pris sur moi de corriger :)

0 votes

Voulez-vous dire dans 3 : Pourquoi static yes check<int>(D*, T=int); est meilleur que static no check(B*, int); ?

112voto

Johannes Schaub - litb Points 256113

S'ils sont liés

Supposons pour un moment que B est en fait une base de D . Puis pour l'appel à check les deux versions sont viables car Host peut être converti en D* y B* . Il s'agit d'une séquence de conversion définie par l'utilisateur et décrite par 13.3.3.1.2 de Host<B, D> a D* y B* respectivement. Pour trouver les fonctions de conversion qui peuvent convertir la classe, les fonctions candidates suivantes sont synthétisées pour la première check selon la fonction 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 fonctions de conversion candidates qui prennent l'objet hôte. La première le prend par référence constante, et la seconde ne le prend pas. Ainsi, la seconde correspond mieux à l'objet non-const. *this (l'objet argument implicite de l'objet ) par 13.3.3.2/3b1sb4 et est utilisé pour convertir en B* pour le deuxième check fonction.

Si vous voulez supprimer le 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 ordinaire de résolution de surcharge, l'appel serait maintenant ambigu parce que normalement le type de retour ne participe 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 aussi bonnes l'une que l'autre, alors leur type de retour décide qui est le meilleur selon 13.3.3/1 . Ainsi, si vous enlevez le const, alors le premier sera pris, car B* se convertit mieux en B* que D* a B* .

Maintenant, quelle séquence de conversion définie par l'utilisateur est la meilleure ? Celle de la deuxième ou de la première fonction de contrôle ? 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, conformément à la norme 13.3.3.2/3b2 . C'est exactement le cas ici : Les deux utilisent la deuxième fonction de conversion. Remarquez qu'ainsi le 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 l'emporte (encore une fois par 13.3.3.2/3b2 ). Dans ce cas, D* se convertit mieux en D* que de B* . Ainsi la première fonction est sélectionnée et nous reconnaissons l'héritage !

Remarquez que puisque nous n'avons jamais eu besoin de en fait convertir en une classe de base, nous pouvons ainsi reconnaître héritage privé parce que si nous pouvons convertir un D* a un B* ne dépend pas de la forme de l'héritage selon 4.10/3

S'ils ne sont pas liés

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 le second, nous avons maintenant une autre série

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

Puisque nous ne pouvons pas convertir D* a 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 ambiguë si ce n'est que la première fonction est un modèle. Les modèles sont un second choix lorsqu'il existe une fonction non-modèle qui est tout aussi bonne selon les critères suivants 13.3.3/1 . Ainsi, nous sélectionnons la fonction non modèle (la deuxième) et nous reconnaissons qu'il n'y a pas d'héritage entre B y D !

2 votes

Ah ! Andreas avait le bon paragraphe, dommage qu'il n'ait pas donné cette réponse :) Merci pour votre temps, j'aimerais pouvoir le mettre en valeur.

2 votes

Cela va être ma réponse préférée de tous les temps... une question : avez-vous lu l'ensemble de la norme C++ ou travaillez-vous seulement au sein du comité C++ ? Félicitations !

4 votes

@DavidKernin travailler dans le comité C++ ne vous fait pas automatiquement connaître le fonctionnement du C++ :) Il faut donc lire la partie de la norme qui est nécessaire pour connaître les détails, ce que j'ai fait. Je ne l'ai pas lu en entier, donc je ne peux certainement pas aider avec la plupart des questions relatives à la bibliothèque standard ou au threading :)

24voto

MSalters Points 74024

Voyons comment cela fonctionne en examinant les étapes.

Commencez par le sizeof(check(Host<B,D>(), int())) partie. Le compilateur peut rapidement voir que cette check(...) est une expression d'appel de fonction, elle doit donc effectuer une résolution de surcharge sur check . Deux surcharges de candidats sont disponibles, template <typename T> yes check(D*, T); y no check(B*, int); . Si le premier est choisi, vous obtenez sizeof(yes) sinon sizeof(no)

Ensuite, regardons la résolution de la surcharge. La première surcharge est une instanciation de template check<int> (D*, T=int) et le deuxième candidat est check(B*, int) . Les arguments réels fournis sont Host<B,D> y int() . Le second paramètre ne les distingue clairement pas ; il sert 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 qui sont nécessaires. Pour la première surcharge, nous avons Host<B,D>::operator D* - une conversion définie par l'utilisateur. Pour la seconde, la surcharge est plus délicate. Nous avons besoin d'un B*, mais il existe deux séquences de conversion possibles. 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 sera Host<B,D>::operator D*() + D*->B* existent. Supposons maintenant que D hérite effectivement de B. Les deux séquences de conversion sont les suivantes Host<B,D> -> Host<B,D> const -> operator B* const -> B* y Host<B,D> -> operator D* -> D* -> B* .

Donc, pour les catégories B et D, no check(<Host<B,D>(), int()) serait ambiguë. Par conséquent, le modèle yes check<int>(D*, int) est choisi. Toutefois, si D n'hérite pas de B, alors no check(<Host<B,D>(), int()) n'est pas ambiguë. À ce stade, la résolution des surcharges ne peut pas se faire sur la base de la séquence de conversion la plus courte. Cependant, à séquences de conversion égales, la résolution de surcharge préfère les fonctions non-modèles, c'est-à-dire no check(B*, int) .

Vous voyez maintenant pourquoi le fait que l'héritage soit privé n'a pas d'importance : cette relation ne sert qu'à éliminer no check(Host<B,D>(), int()) de la résolution des surcharges avant que le contrôle d'accès n'ait lieu. Et vous voyez également pourquoi le operator B* const doit être const : sinon, il n'y a pas besoin de l'attribut Host<B,D> -> Host<B,D> const étape, aucune ambiguïté, et no check(B*, int) serait toujours choisi.

0 votes

Votre explication ne tient pas compte de la présence const . Si votre réponse est vraie, alors non const est nécessaire. Mais ce n'est pas vrai. Supprimez const et l'astuce ne fonctionnera pas.

0 votes

Sans la constante, les deux séquences de conversion pour no check(B*, int) ne sont plus ambiguës.

0 votes

Si vous laissez seulement no check(B*, int) alors, pour les B y D il n'y aurait pas d'ambiguïté. Le compilateur choisirait sans ambiguïté operator D*() pour effectuer la conversion car elle n'a pas de const. C'est plutôt dans le sens inverse : Si vous supprimer la const, vous introduisez un certain sens de l'ambiguïté, mais qui est résolu par le fait que operator B*() fournit un type de retour supérieur qui n'a pas besoin d'une conversion de pointeur en B* comme D* fait.

5voto

Matthieu M. Points 101624

Le site private est complètement ignoré par is_base_of car la résolution des surcharges intervient avant les contrôles 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
}

Il en va de même ici, le fait que B est une base privée n'empêche pas la vérification d'avoir lieu, elle empêcherait seulement la conversion, mais nous ne demandons jamais la conversion réelle ;)

0 votes

En quelque sorte. Aucune conversion de base n'est effectuée. host est arbitrairement converti en D* o B* dans l'expression non évaluée. Pour une raison quelconque, D* est préférable à B* sous certaines conditions.

0 votes

Je pense que la réponse se trouve dans le paragraphe 13.3.1.1.2, mais je dois encore régler les détails :)

0 votes

Ma réponse n'explique que la partie "pourquoi même le privé fonctionne", la réponse de sellibitze est certainement plus complète bien que j'attende avec impatience une explication claire du processus complet de résolution selon les cas.

2voto

sellibitze Points 13607

Cela a peut-être quelque chose à voir avec l'ordre partiel de résolution des surcharges. D* est plus spécialisé que B* dans le cas où D dérive de B.

Les détails exacts sont assez compliqués. Vous devez déterminer les préséances des différentes règles de résolution des surcharges. L'ordre partiel en est une. Les longueurs/types de séquences de conversion en sont une autre. Enfin, si deux fonctions viables sont jugées de qualité égale, les non-modèles sont préférés aux modèles de fonctions.

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 des surcharges. Lorsque D ne dérive pas de B, les règles d'ordre 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 au fait que l'héritage soit prive : le code ne demande jamais une conversion de D* en B* qui nécessiterait un héritage public.

0 votes

Je pense que c'est quelque chose comme ça, je me souviens avoir vu une discussion approfondie sur les archives de boost à propos de l'implémentation de is_base_of et les boucles par lesquelles les contributeurs sont passés pour s'en assurer.

0 votes

The exact details are rather complicated - c'est le but. S'il vous plaît, expliquez. Je veux savoir.

0 votes

Alexey : Eh bien, je pensais vous avoir orienté dans la bonne direction. Vérifiez comment les différentes règles de résolution des surcharges interagissent dans ce cas. La seule différence entre D dérivant de B et D ne dérivant pas de B en ce qui concerne la résolution de ce cas de surcharge est la règle d'ordonnancement partiel. La résolution des surcharges est décrite dans le §13 de la norme C++. Vous pouvez obtenir un brouillon gratuitement : open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf

0voto

Hertz Points 21

En suivant votre deuxième question, notez que si ce n'était pas pour const, Host serait mal formé si instancié avec B == D. Mais is_base_of est conçu de telle sorte que chaque classe est une base d'elle-même, donc un des opérateurs de conversion doit être const.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X