Comment fonctionne `is_base_of`?

118

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 Bc'est une base privée. Comment cela marche-t-il?

  2. Notez que operator B*()c'est const. Pourquoi c'est important?

  3. Pourquoi est-ce template<typename T> static yes check(D*, T);mieux 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.

Alexey Malistov
la source
4
C'est très déroutant de votre part d'utiliser le même identifiant pour un paramètre de template et un vrai nom de classe ...
Matthieu M.
1
@Matthieu M., j'ai pris l'initiative de corriger :)
Kirill V. Lyadvinsky
2
Il y a quelque temps, j'ai écrit une implémentation alternative de is_base_of: ideone.com/T0C1V Cela ne fonctionne pas avec les anciennes versions de GCC (GCC4.3 fonctionne bien).
Johannes Schaub - litb
3
Ok, je vais me promener.
jokoon le
2
Cette implémentation n'est pas correcte. is_base_of<Base,Base>::valuedevrait être true; cela revient false.
chengiz

Réponses:

109

S'ils sont liés

Supposons un instant qu'il Bs'agit en fait d'une base de D. Ensuite, pour l'appel à check, les deux versions sont viables car elles Hostpeuvent être converties en D* et B* . C'est une séquence de conversion définie par l'utilisateur comme décrit par 13.3.3.1.2de 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 checkfonction selon13.3.1.5/1

D* (Host<B, D>&)

La première fonction de conversion n'est pas candidate, car elle B*ne peut pas être convertie 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 de fonction de conversion qui prennent l'objet hôte. Le premier le prend par référence const, et le second non. Ainsi, le second est une meilleure correspondance pour l' *thisobjet non-const (l' argument d'objet implicite ) par 13.3.3.2/3b1sb4et est utilisé pour convertir en B*pour la seconde checkfonction.

Si vous supprimiez 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 constness. 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, le type de retour de celles-ci décide de qui est le meilleur selon 13.3.3/1. Ainsi, si vous supprimiez le const, alors le premier serait pris, car B*convertit mieux en B*que D*en B*.

Quelle est la meilleure séquence de conversion définie par l'utilisateur? Celui 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 que selon 13.3.3.2/3b2. C'est exactement le cas ici: les deux utilisent la deuxième fonction de conversion. Notez qu'ainsi le const est important car il force 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 vers le type de destination l'emporte (à nouveau 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 on reconnaît l'héritage!

Notez que puisque nous n'avons jamais eu besoin de convertir réellement en une classe de base, nous pouvons ainsi reconnaître l'héritage privé car la possibilité de convertir de a D*en a B*ne dépend pas de la forme d'héritage selon4.10/3

S'ils ne sont pas liés

Supposons maintenant qu'ils ne soient 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 entre les deux séquences de conversion définies par l'utilisateur! Ainsi, nous serions ambigus si ce n'était du 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 tout aussi 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 Bet D!

Johannes Schaub - litb
la source
2
Ah! Andreas avait le bon paragraphe, dommage qu'il n'ait pas donné une telle réponse :) Merci pour votre temps, j'aimerais pouvoir le mettre en favori.
Matthieu M.
2
Cela va être ma réponse préférée de tous les temps ... une question: avez-vous lu toute la norme C ++ ou travaillez-vous simplement dans le comité C ++ ?? Toutes nos félicitations!
Marco A.
4
@DavidKernin travaillant dans le comité C ++ ne vous fait pas automatiquement savoir comment fonctionne C ++ :) Vous devez donc absolument lire la partie du Standard qui est nécessaire pour connaître les détails, ce que j'ai fait. Je n'ai pas lu tout cela, donc je ne peux certainement pas aider avec la plupart des questions liées à la bibliothèque Standard ou aux
threads
1
@underscore_d Pour être honnête, la spécification n'interdit pas aux traits std :: d'utiliser la magie du compilateur afin que les implémenteurs de bibliothèques standard puissent les utiliser comme ils le souhaitent . Ils éviteront les acrobaties du modèle qui aident également à accélérer le temps de compilation et l'utilisation de la mémoire. Cela est vrai même si l' interface ressemble à std::is_base_of<...>. Tout est sous le capot.
Johannes Schaub - litb
2
Bien sûr, les bibliothèques générales comme boost::doivent s'assurer qu'elles ont ces intrinsèques disponibles avant de les utiliser. Et j'ai le sentiment qu'il y a une sorte de mentalité de "défi accepté" parmi eux pour implémenter des choses sans l'aide du compilateur :)
Johannes Schaub - litb
24

Voyons comment cela fonctionne en regardant les étapes.

Commencez par la sizeof(check(Host<B,D>(), int()))pièce. Le compilateur peut voir rapidement qu'il check(...)s'agit d'une expression d'appel de fonction, il doit donc effectuer une résolution de surcharge sur check. Il existe deux surcharges candidates disponibles, template <typename T> yes check(D*, T);et no check(B*, int);. Si le premier est choisi, vous obtenez sizeof(yes), sinonsizeof(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 second candidat l'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; cela a simplement servi à faire de la première surcharge un modèle. Nous verrons plus tard pourquoi la partie modèle est pertinente.

Examinez 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'un 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-t-elle. Supposons maintenant que D hérite effectivement 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*.

Donc, pour B et D liés, no check(<Host<B,D>(), int())serait ambigu. En conséquence, le modèle yes check<int>(D*, int)est choisi. 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, à séquences de conversion égales, la résolution de surcharge préfère les fonctions non-modèle, c'est-à-dire no check(B*, int).

Vous voyez maintenant pourquoi peu importe 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* constdoit être const: sinon il n'y a pas besoin de l' Host<B,D> -> Host<B,D> constétape, pas d'ambiguïté, et no check(B*, int)serait toujours choisi.

MSalters
la source
Votre explication ne tient pas compte de la présence de const. Si votre réponse est vraie, non const. Mais ce n'est pas vrai. Supprimer constet tromper ne fonctionnera pas.
Alexey Malistov
Sans le const, les deux séquences de conversion pour no check(B*, int)ne sont plus ambiguës.
MSalters
Si vous partez seulement no check(B*, int), alors pour lié Bet D, ce ne serait pas ambigu. Le compilateur choisirait sans ambiguïté operator D*()d'effectuer la conversion car il n'a pas de const. C'est plutôt un peu dans la direction opposée: si vous supprimez le const, vous introduisez une certaine ambiguïté, mais qui est résolue par le fait qu'il operator B*()fournit un type de retour supérieur qui n'a pas besoin d'une conversion de pointeur pour B*like D*do.
Johannes Schaub - litb
C'est en effet le point: l'ambiguïté se situe entre les deux séquences de conversion différentes pour obtenir un B*du <Host<B,D>()temporaire.
MSalters
C'est une meilleure réponse. Merci! Donc, comme je l'ai compris, si une fonction est meilleure, mais ambiguë, alors une autre fonction est choisie?
user1289
4

Le privatebit est complètement ignoré is_base_ofcar la résolution de surcharge se produit avant les vérifications d'accessibilité.

Vous pouvez vérifier cela 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 qu'il Bs'agisse d'une base privée n'empêche pas le contrôle d'avoir lieu, cela empêcherait seulement la conversion, mais nous ne demandons jamais la conversion proprement dite;)

Matthieu M.
la source
Sorte de. Aucune conversion de base n'est effectuée du tout. hostest arbitrairement converti en D*ou B*dans l'expression non évaluée. Pour une raison quelconque, D*est préférable à B*certaines conditions.
Potatoswatter
Je pense que la réponse est en 13.3.1.1.2 mais je n'ai pas encore réglé les détails :)
Andreas Brinck
Ma réponse n'explique que la partie «pourquoi même les travaux privés», la réponse de sellibitze est certainement plus complète même si j'attends avec impatience une explication claire du processus de résolution complète en fonction des cas.
Matthieu M.
2

Cela a peut-être quelque chose à voir avec un ordre partiel de 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 une. Les longueurs / types de séquences de conversion en est un autre. Enfin, si deux fonctions viables sont jugées également bonnes, les non-modèles sont choisis par rapport 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 de surcharge. Lorsque D ne dérive pas de B, les règles de tri 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 un héritage public.

sellibitze
la source
Je pense que c'est quelque chose comme ça, je me souviens avoir vu une discussion approfondie sur les archives de boost sur la mise en œuvre is_base_ofet les boucles que les contributeurs ont traversées pour assurer cela.
Matthieu M.
The exact details are rather complicated- c'est le but. S'il vous plaît, expliquez. Je veux savoir.
Alexey Malistov
@Alexey: Eh bien, je pensais vous avoir orienté dans la bonne direction. Découvrez comment les différentes règles de résolution de surcharge interagissent dans ce cas. La seule différence entre D dérivant de B et D ne dérivant pas de B par rapport à la résolution de ce cas de surcharge est la règle de classement partiel. La résolution de surcharge est décrite au §13 de la norme C ++. Vous pouvez obtenir un brouillon gratuitement: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
La résolution de surcharge s'étend sur 16 pages dans ce brouillon. Je suppose que si vous avez vraiment besoin de comprendre les règles et l'interaction entre elles pour ce cas, vous devriez lire la section complète §13.3. Je ne compte pas obtenir ici une réponse correcte à 100% et conforme à vos normes.
sellibitze
s'il vous plaît voir ma réponse pour une explication de celui-ci si vous êtes intéressé.
Johannes Schaub - litb
0

À la suite de 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 d'elle-même, donc l'un des opérateurs de conversion doit être const.

Hertz
la source