Wie funktioniert `is_base_of`?

118

Wie funktioniert der folgende Code?

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. Beachten Sie, dass dies Beine private Basis ist. Wie funktioniert das?

  2. Beachten Sie, dass operator B*()const ist. Warum ist es wichtig?

  3. Warum ist template<typename T> static yes check(D*, T);besser als static yes check(B*, int);?

Hinweis : Es handelt sich um eine reduzierte Version (Makros werden entfernt) von boost::is_base_of. Und das funktioniert auf einer Vielzahl von Compilern.

Alexey Malistov
quelle
4
Es ist sehr verwirrend von Ihnen, den gleichen Bezeichner für einen Vorlagenparameter und einen wahren Klassennamen zu verwenden ...
Matthieu M.
1
@Matthieu M., ich habe es auf mich genommen, zu korrigieren :)
Kirill V. Lyadvinsky
2
Vor einiger Zeit habe ich eine alternative Implementierung von geschrieben is_base_of: ideone.com/T0C1V Es funktioniert jedoch nicht mit älteren GCC-Versionen (GCC4.3 funktioniert einwandfrei).
Johannes Schaub - litb
3
Ok, ich gehe spazieren.
Jokoon
2
Diese Implementierung ist nicht korrekt. is_base_of<Base,Base>::valuesollte sein true; dies kehrt zurück false.
Chengiz

Antworten:

109

Wenn sie verwandt sind

Nehmen wir für einen Moment an, dass dies Btatsächlich eine Basis von ist D. Für den Aufruf von checksind dann beide Versionen realisierbar, da Hostsie in D* und konvertiert werden können B*. Es ist eine frei wählbare Konvertierungssequenz wie beschrieben 13.3.3.1.2aus Host<B, D>an D*und B*verbunden. Um Konvertierungsfunktionen zu finden, die die Klasse konvertieren können, werden die folgenden Kandidatenfunktionen für die erste checkFunktion gemäß synthetisiert13.3.1.5/1

D* (Host<B, D>&)

Die erste Konvertierungsfunktion ist kein Kandidat, da B*sie nicht konvertiert werden kann D*.

Für die zweite Funktion existieren folgende Kandidaten:

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

Dies sind die beiden Kandidaten für die Konvertierungsfunktion, die das Hostobjekt übernehmen. Der erste nimmt es als konstante Referenz und der zweite nicht. Somit ist die zweite eine bessere Übereinstimmung für das nicht konstante *thisObjekt (das implizite Objektargument ) von 13.3.3.2/3b1sb4und wird zum Konvertieren B*für die zweite checkFunktion verwendet.

Wenn Sie die Konstante entfernen würden , hätten wir die folgenden Kandidaten

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

Dies würde bedeuten, dass wir nicht mehr nach Konstanz auswählen können. In einem normalen Überlastungsauflösungsszenario wäre der Aufruf jetzt mehrdeutig, da der Rückgabetyp normalerweise nicht an der Überlastungsauflösung beteiligt ist. Für Konvertierungsfunktionen gibt es jedoch eine Hintertür. Wenn zwei Konvertierungsfunktionen gleich gut sind, entscheidet der Rückgabetyp, wer der beste ist 13.3.3/1. Wenn Sie also die Konstante entfernen würden, würde die erste verwendet, da sie B*besser in B*als D*in konvertiert B*.

Welche benutzerdefinierte Konvertierungssequenz ist nun besser? Die für die zweite oder die erste Prüffunktion? Die Regel ist, dass benutzerdefinierte Konvertierungssequenzen nur verglichen werden können, wenn sie dieselbe Konvertierungsfunktion oder denselben Konstruktor gemäß verwenden 13.3.3.2/3b2. Genau das ist hier der Fall: Beide verwenden die zweite Konvertierungsfunktion. Beachten Sie, dass die const daher wichtig ist, da sie den Compiler zwingt, die zweite Konvertierungsfunktion zu übernehmen.

Da können wir sie vergleichen - welches ist besser? Die Regel ist, dass die bessere Konvertierung vom Rückgabetyp der Konvertierungsfunktion in den Zieltyp (erneut von 13.3.3.2/3b2) gewinnt . In diesem Fall D*konvertiert besser nach D*als nach B*. Damit ist die erste Funktion ausgewählt und wir erkennen die Vererbung!

Beachten Sie, dass wir, da wir nie tatsächlich in eine Basisklasse konvertieren mussten, die private Vererbung erkennen können, da es nicht von der Form der Vererbung abhängt, ob wir von a D*nach a konvertieren könnenB*4.10/3

Wenn sie nicht verwandt sind

Nehmen wir nun an, sie sind nicht durch Vererbung verbunden. Somit haben wir für die erste Funktion die folgenden Kandidaten

D* (Host<B, D>&) 

Und zum zweiten haben wir jetzt ein anderes Set

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

Da wir nicht konvertieren können D*, B*wenn wir keine Vererbungsbeziehung haben, haben wir jetzt keine gemeinsame Konvertierungsfunktion zwischen den beiden benutzerdefinierten Konvertierungssequenzen! Daher wären wir nicht eindeutig, wenn nicht die erste Funktion eine Vorlage wäre. Vorlagen sind die zweite Wahl, wenn es eine Nicht-Vorlagenfunktion gibt, die entsprechend gut ist 13.3.3/1. Daher wählen wir die Nicht-Template-Funktion (zweite) aus und erkennen, dass zwischen Bund D! Keine Vererbung besteht.

Johannes Schaub - litb
quelle
2
Ah! Andreas hatte den Absatz richtig, schade, dass er keine solche Antwort gegeben hat :) Danke für deine Zeit, ich wünschte, ich könnte es als Favorit bezeichnen.
Matthieu M.
2
Dies wird meine Lieblingsantwort sein ... eine Frage: Haben Sie den gesamten C ++ - Standard gelesen oder arbeiten Sie nur im C ++ - Komitee? Herzliche Glückwünsche!
Marco A.
4
@DavidKernin, das im C ++ - Komitee arbeitet, lässt Sie nicht automatisch wissen, wie C ++ funktioniert :) Sie müssen also unbedingt den Teil des Standards lesen, der erforderlich ist, um die Details zu kennen, die ich getan habe. Ich habe noch nicht alles gelesen, daher kann ich bei den meisten Fragen zur Standardbibliothek oder zum Threading definitiv nicht helfen :)
Johannes Schaub - 2.
1
@underscore_d Um fair zu sein, verbietet die Spezifikation den std :: -Eigenschaften nicht, Compilermagie zu verwenden, damit Standardbibliotheksimplementierer sie nach Belieben verwenden können . Sie vermeiden die Vorlagenakrobatik, was auch die Kompilierungszeit und die Speichernutzung beschleunigt. Dies gilt auch dann, wenn die Benutzeroberfläche so aussieht std::is_base_of<...>. Es ist alles unter der Haube.
Johannes Schaub - Litb
2
Natürlich müssen allgemeine Bibliotheken wie boost::diese sicherstellen, dass diese Eigenschaften verfügbar sind, bevor sie verwendet werden. Und ich habe das Gefühl, dass es unter ihnen eine Art "Herausforderung angenommene" Mentalität gibt, Dinge ohne die Hilfe des Compilers zu implementieren :)
Johannes Schaub - litb
24

Lassen Sie uns anhand der Schritte herausfinden, wie es funktioniert.

Beginnen Sie mit dem sizeof(check(Host<B,D>(), int()))Teil. Der Compiler kann schnell erkennen, dass dies check(...)ein Funktionsaufrufausdruck ist, daher muss die Überlastungsauflösung aktiviert werden check. Es stehen zwei Kandidatenüberladungen zur Verfügung, template <typename T> yes check(D*, T);und no check(B*, int);. Wenn der erste gewählt wird, erhalten Sie sizeof(yes)sonstsizeof(no)

Schauen wir uns als nächstes die Überlastungsauflösung an. Die erste Überladung ist eine Vorlageninstanziierung check<int> (D*, T=int)und der zweite Kandidat ist check(B*, int). Die tatsächlich angegebenen Argumente sind Host<B,D>und int(). Der zweite Parameter unterscheidet sie eindeutig nicht; es diente lediglich dazu, die erste Überladung zu einer Vorlage zu machen. Wir werden später sehen, warum der Vorlagenteil relevant ist.

Schauen Sie sich nun die benötigten Konvertierungssequenzen an. Für die erste Überladung haben wir Host<B,D>::operator D*- eine benutzerdefinierte Konvertierung. Zum zweiten ist die Überlastung schwieriger. Wir brauchen ein B *, aber es gibt möglicherweise zwei Konvertierungssequenzen. Einer ist über Host<B,D>::operator B*() const. Wenn (und nur wenn) B und D durch Vererbung zusammenhängen, existiert die Konvertierungssequenz Host<B,D>::operator D*()+ D*->B*. Nehmen wir nun an, D erbt tatsächlich von B. Die beiden Konvertierungssequenzen sind Host<B,D> -> Host<B,D> const -> operator B* const -> B*und Host<B,D> -> operator D* -> D* -> B*.

Also, für verwandte B und D no check(<Host<B,D>(), int())wäre mehrdeutig. Infolgedessen wird die Vorlage yes check<int>(D*, int)ausgewählt. Wenn D jedoch nicht von B erbt, no check(<Host<B,D>(), int())ist es nicht mehrdeutig. Zu diesem Zeitpunkt kann die Überlastungsauflösung nicht basierend auf der kürzesten Konvertierungssequenz erfolgen. Bei gleichen Konvertierungssequenzen bevorzugt die Überlastungsauflösung jedoch Funktionen ohne Vorlage, d no check(B*, int). H.

Sie sehen jetzt, warum es keine Rolle spielt, dass die Vererbung privat ist: Diese Beziehung dient nur dazu, die no check(Host<B,D>(), int())Überlastungsauflösung zu beseitigen, bevor die Zugriffsprüfung stattfindet. Und Sie sehen auch, warum das operator B* constconst sein muss: Sonst gibt es keine Notwendigkeit für den Host<B,D> -> Host<B,D> constSchritt, keine Mehrdeutigkeit und no check(B*, int)würde immer gewählt werden.

MSalters
quelle
Ihre Erklärung berücksichtigt nicht das Vorhandensein von const. Wenn Ihre Antwort wahr ist, wird nein constbenötigt. Aber es ist nicht wahr. Entfernen constund Trick wird nicht funktionieren.
Alexey Malistov
Ohne die Konstante sind die beiden Konvertierungssequenzen für no check(B*, int)nicht mehr mehrdeutig.
MSalters
Wenn Sie nur gehen no check(B*, int), dann für verwandte Bund D, wäre es nicht mehrdeutig. Der Compiler würde operator D*()die Konvertierung eindeutig durchführen, da er keine Konstante hat. Es ist eher ein bisschen in der entgegengesetzten Richtung: Wenn Sie entfernen die const, Sie einführen ein Gefühl der Mehrdeutigkeit, die aber durch die Tatsache gelöst, die operator B*()einen überlegenen Rückgabetyp bereitstellt , die keinen Zeiger Konvertierung braucht B*wie der D*Fall ist.
Johannes Schaub - litb
Das ist in der Tat der Punkt: Die Mehrdeutigkeit besteht zwischen den beiden verschiedenen Konvertierungssequenzen, um eine B*aus der <Host<B,D>()temporären zu erhalten.
MSalters
Dies ist eine bessere Antwort. Vielen Dank! Also, wie ich verstanden habe, wenn eine Funktion besser, aber mehrdeutig ist, dann wird eine andere Funktion gewählt?
user1289
4

Das privateBit wird von vollständig ignoriert, is_base_ofda vor der Zugänglichkeitsprüfung eine Überlastungsauflösung auftritt.

Sie können dies einfach überprüfen:

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
}

Gleiches gilt hier, die Tatsache, dass Bes sich um eine private Basis handelt, verhindert nicht die Überprüfung, sondern nur die Konvertierung, aber wir fragen nie nach der tatsächlichen Konvertierung;)

Matthieu M.
quelle
Art von. Es wird überhaupt keine Basiskonvertierung durchgeführt. hostwird willkürlich in D*oder B*in den nicht bewerteten Ausdruck konvertiert . Aus irgendeinem Grund D*ist B*unter bestimmten Bedingungen vorzuziehen .
Potatoswatter
Ich denke, die Antwort ist in 13.3.1.1.2, aber ich muss noch die Details klären :)
Andreas Brinck
Meine Antwort erklärt nur den Teil "Warum auch private Werke". Die Antwort von sellibitze ist sicherlich vollständiger, obwohl ich gespannt auf eine klare Erklärung des vollständigen Lösungsprozesses in Abhängigkeit von den Fällen warte.
Matthieu M.
2

Es hat möglicherweise etwas mit der teilweisen Bestellung der Überlastungsauflösung zu tun. D * ist spezialisierter als B *, falls D von B stammt.

Die genauen Details sind ziemlich kompliziert. Sie müssen die Vorrangstellung verschiedener Überlastungsauflösungsregeln herausfinden. Teilbestellung ist eine. Längen / Arten von Konvertierungssequenzen sind eine andere. Wenn schließlich zwei realisierbare Funktionen als gleich gut angesehen werden, werden Nichtvorlagen gegenüber Funktionsvorlagen ausgewählt.

Ich musste nie nachsehen, wie diese Regeln interagieren. Es scheint jedoch, dass eine teilweise Reihenfolge die anderen Regeln für die Überlastungsauflösung dominiert. Wenn D nicht von B abgeleitet ist, gelten die Teilbestellregeln nicht und die Nichtvorlage ist attraktiver. Wenn D von B abgeleitet ist, setzt die Teilreihenfolge ein und macht die Funktionsvorlage attraktiver - wie es scheint.

Wenn die Vererbung privat ist: Der Code fordert niemals eine Konvertierung von D * nach B * an, die eine öffentliche Vererbung erfordern würde.

sellibitze
quelle
Ich denke, es ist so etwas. Ich erinnere mich, dass ich eine ausführliche Diskussion über die Boost-Archive über die Implementierung is_base_ofund die Schleifen gesehen habe, die die Mitwirkenden durchlaufen haben, um dies sicherzustellen.
Matthieu M.
The exact details are rather complicated- das ist der Punkt. Bitte erkläre. Ich möchte es wissen.
Alexey Malistov
@ Alexander: Nun, ich dachte, ich hätte dich in die richtige Richtung gelenkt. Überprüfen Sie, wie die verschiedenen Regeln für die Überlastungsauflösung in diesem Fall zusammenwirken. Der einzige Unterschied zwischen D, der von B stammt, und D, der nicht von B stammt, in Bezug auf die Auflösung dieses Überlastungsfalls ist die Teilordnungsregel. Die Überlastungsauflösung ist in §13 des C ++ - Standards beschrieben. Sie können einen Entwurf kostenlos erhalten: open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1804.pdf
sellibitze
Die Überlastungsauflösung umfasst 16 Seiten in diesem Entwurf. Ich denke, wenn Sie die Regeln und die Interaktion zwischen ihnen für diesen Fall wirklich verstehen müssen, sollten Sie den vollständigen Abschnitt §13.3 lesen. Ich würde nicht damit rechnen, hier eine Antwort zu bekommen, die 100% korrekt ist und Ihren Standards entspricht.
Sellibitze
Bitte lesen Sie meine Antwort für eine Erklärung, wenn Sie interessiert sind.
Johannes Schaub - litb
0

Beachten Sie bei Ihrer zweiten Frage, dass Host ohne const schlecht geformt wäre, wenn es mit B == D instanziiert würde. Is_base_of ist jedoch so konzipiert, dass jede Klasse eine Basis für sich selbst ist, daher muss einer der Konvertierungsoperatoren sei const.

Hertz
quelle