Warum muss ich über diesen Zeiger auf Mitglieder der Vorlagenbasisklasse zugreifen?

199

Wenn die folgenden Klassen keine Vorlagen wären, könnte ich sie einfach xin der derivedKlasse haben. Mit dem folgenden Code muss ich jedoch verwenden this->x. Warum?

template <typename T>
class base {

protected:
    int x;
};

template <typename T>
class derived : public base<T> {

public:
    int f() { return this->x; }
};

int main() {
    derived<int> d;
    d.f();
    return 0;
}
Ali
quelle
1
Ah je. Es hat etwas mit der Namenssuche zu tun. Wenn jemand dies nicht bald beantwortet, werde ich es nachschlagen und veröffentlichen (jetzt beschäftigt).
Templatetypedef
@ Ed Swangren: Entschuldigung, ich habe es unter den angebotenen Antworten beim Posten dieser Frage verpasst. Ich hatte schon lange vorher nach der Antwort gesucht.
Ali
6
Dies geschieht aufgrund der zweiphasigen Namenssuche (die standardmäßig nicht von allen Compilern verwendet wird) und abhängigen Namen. Es gibt 3 Lösungen für dieses Problem, außer dem Präfix xmit this->: 1) Verwenden Sie das Präfix base<T>::x, 2) Fügen Sie eine Anweisung hinzuusing base<T>::x , 3) Verwenden Sie einen globalen Compiler-Schalter, der den zulässigen Modus aktiviert. Die Vor- und Nachteile dieser Lösungen sind in stackoverflow.com/questions/50321788/…
George Robinson

Antworten:

274

Kurze Antwort: Um xeinen abhängigen Namen zu erstellen, wird die Suche verschoben, bis der Vorlagenparameter bekannt ist.

Lange Antwort: Wenn ein Compiler eine Vorlage sieht, soll er bestimmte Überprüfungen sofort durchführen, ohne den Vorlagenparameter zu sehen. Andere werden verschoben, bis der Parameter bekannt ist. Es wird als zweiphasige Kompilierung bezeichnet, und MSVC tut dies nicht, aber es wird vom Standard verlangt und von den anderen großen Compilern implementiert. Wenn Sie möchten, muss der Compiler die Vorlage kompilieren, sobald er sie sieht (zu einer Art interner Analysebaumdarstellung), und die Kompilierung der Instanziierung auf später verschieben.

Die Überprüfungen, die an der Vorlage selbst und nicht an bestimmten Instanziierungen durchgeführt werden, erfordern, dass der Compiler in der Lage ist, die Grammatik des Codes in der Vorlage aufzulösen.

In C ++ (und C) müssen Sie manchmal wissen, ob etwas ein Typ ist oder nicht, um die Grammatik des Codes aufzulösen. Beispielsweise:

#if WANT_POINTER
    typedef int A;
#else
    int A;
#endif
static const int x = 2;
template <typename T> void foo() { A *x = 0; }

Wenn A ein Typ ist, deklariert dies einen Zeiger (ohne andere Wirkung als das Schatten des Globalen x). Wenn A ein Objekt ist, ist dies eine Multiplikation (und wenn ein Operator nicht überlastet wird, ist dies unzulässig und wird einem r-Wert zugewiesen). Wenn es falsch ist, muss dieser Fehler in Phase 1 diagnostiziert werden. Der Standard definiert ihn als Fehler in der Vorlage und nicht in einer bestimmten Instanziierung. Selbst wenn die Vorlage niemals instanziiert wird, wenn A ein ist, intist der obige Code falsch geformt und muss diagnostiziert werden, so wie es wäre, wenn überhaupt fookeine Vorlage, sondern eine einfache Funktion wäre.

Der Standard besagt nun, dass Namen, die nicht von Vorlagenparametern abhängig sind, in Phase 1 auflösbar sein müssen. AHier handelt es sich nicht um einen abhängigen Namen, sondern um dasselbe, unabhängig vom Typ T. Es muss also definiert werden, bevor die Vorlage definiert wird, um in Phase 1 gefunden und überprüft zu werden.

T::Awäre ein Name, der von T abhängt. Wir können unmöglich in Phase 1 wissen, ob das ein Typ ist oder nicht. Der Typ, der Twahrscheinlich wie in einer Instanziierung verwendet wird, ist noch nicht einmal definiert, und selbst wenn dies der Fall wäre, wissen wir nicht, welche Typen als Vorlagenparameter verwendet werden. Aber wir müssen die Grammatik auflösen, um unsere wertvollen Phase-1-Prüfungen auf schlecht geformte Vorlagen durchzuführen. Der Standard hat also eine Regel für abhängige Namen - der Compiler muss davon ausgehen, dass es sich nicht um Typen handelt, es sei denn, er ist qualifiziert, typenameum anzugeben, dass es sich um Typen handelt, oder er wird in bestimmten eindeutigen Kontexten verwendet. Zum Beispiel wird in template <typename T> struct Foo : T::A {};, T::Aals Basisklasse verwendet und ist daher eindeutig ein Typ. If Foowird mit einem Typ instanziiert, der ein Datenelement hatA Anstelle eines verschachtelten Typs A ist dies ein Fehler im Code, der die Instanziierung durchführt (Phase 2), nicht ein Fehler in der Vorlage (Phase 1).

Aber was ist mit einer Klassenvorlage mit einer abhängigen Basisklasse?

template <typename T>
struct Foo : Bar<T> {
    Foo() { A *x = 0; }
};

Ist A ein abhängiger Name oder nicht? Bei Basisklassen kann ein beliebiger Name in der Basisklasse angezeigt werden. Wir könnten also sagen, dass A ein abhängiger Name ist, und ihn als Nicht-Typ behandeln. Dies hätte den unerwünschten Effekt, dass jeder Name in Foo abhängig ist und daher jeder in Foo verwendete Typ (mit Ausnahme der eingebauten Typen) qualifiziert werden muss. Innerhalb von Foo müssten Sie schreiben:

typename std::string s = "hello, world";

da std::stringwäre ein abhängiger Name und würde daher als Nicht-Typ angenommen, sofern nicht anders angegeben. Autsch!

Ein zweites Problem beim Zulassen Ihres bevorzugten Codes ( return x;) besteht darin, dass jemand , selbst wenn er Barzuvor definiert Foowurde und xkein Mitglied dieser Definition ist, später eine Spezialisierung Barfür einen bestimmten Typ definieren kann Baz, z. B. Bar<Baz>ein Datenelement x, und dann instanziiert Foo<Baz>. In dieser Instanziierung würde Ihre Vorlage das Datenelement zurückgeben, anstatt das globale Element zurückzugeben x. Oder umgekehrt, wenn die Basisvorlagendefinition von Barhattex eine Spezialisierung ohne diese definieren würde und Ihre Vorlage nach einer globalen Version suchen würde, xin die sie zurückkehren könnte Foo<Baz>. Ich denke, dies wurde als genauso überraschend und beunruhigend beurteilt wie das Problem, das Sie haben, aber es ist stillschweigend überraschend, im Gegensatz zu einem überraschenden Fehler.

Um diese Probleme zu vermeiden, besagt der Standard, dass abhängige Basisklassen von Klassenvorlagen nur dann für die Suche berücksichtigt werden, wenn dies ausdrücklich angefordert wird. Dies verhindert, dass alles abhängig ist, nur weil es in einer abhängigen Basis gefunden werden kann. Es hat auch den unerwünschten Effekt, den Sie sehen - Sie müssen Dinge aus der Basisklasse qualifizieren oder es wird nicht gefunden. Es gibt drei Möglichkeiten, Aabhängig zu machen :

  • using Bar<T>::A;in der Klasse - Abezieht sich jetzt auf etwas in Bar<T>, daher abhängig.
  • Bar<T>::A *x = 0;am Point of Use - Wieder Aist definitiv in Bar<T>. Dies ist eine Multiplikation, da sie typenamenicht verwendet wurde, also möglicherweise ein schlechtes Beispiel, aber wir müssen bis zur Instanziierung warten, um herauszufinden, ob operator*(Bar<T>::A, x)ein r-Wert zurückgegeben wird. Wer weiß, vielleicht tut es das ...
  • this->A;at point of use - Aist ein Mitglied. Wenn es also nicht in ist Foo, muss es in der Basisklasse sein. Wieder sagt der Standard, dass dies es abhängig macht.

Die zweiphasige Kompilierung ist umständlich und schwierig und führt einige überraschende Anforderungen für zusätzliche Redewendungen in Ihren Code ein. Aber ähnlich wie bei der Demokratie ist es wahrscheinlich die schlechteste Art, Dinge zu tun, abgesehen von allen anderen.

Sie könnten vernünftigerweise argumentieren, dass in Ihrem Beispiel return x;kein Sinn ergibt, wennx es sich um einen verschachtelten Typ in der Basisklasse handelt. Daher sollte die Sprache (a) sagen, dass es sich um einen abhängigen Namen handelt, und (2) ihn als Nicht-Typ behandeln und Ihr Code würde ohne funktionieren this->. Bis zu einem gewissen Grad sind Sie das Opfer von Kollateralschäden durch die Lösung eines Problems, das in Ihrem Fall nicht zutrifft, aber es gibt immer noch das Problem, dass Ihre Basisklasse möglicherweise Namen unter Ihnen einführt, die Globals beschatten, oder keine Namen hat, die Sie gedacht haben sie hatten und stattdessen ein globales Wesen gefunden.

Sie könnten möglicherweise auch argumentieren, dass die Standardeinstellung für abhängige Namen das Gegenteil sein sollte (Typ annehmen, sofern nicht irgendwie angegeben, dass es sich um ein Objekt handelt), oder dass die Standardeinstellung kontextsensitiver sein sollte (in std::string s = "";, std::stringkönnte als Typ gelesen werden, da nichts anderes grammatikalisch macht Sinn, obwohl std::string *s = 0;mehrdeutig ist). Auch hier weiß ich nicht genau, wie die Regeln vereinbart wurden. Ich vermute, dass die Anzahl der erforderlichen Textseiten verringert wird, um viele spezifische Regeln zu erstellen, für die Kontexte einen Typ annehmen und für welche nicht.

Steve Jessop
quelle
1
Ooh, schöne ausführliche Antwort. Ich habe ein paar Dinge geklärt, die ich nie nachgeschlagen habe. :) +1
Jalf
20
@jalf: Gibt es so etwas wie das C ++ QTWBFAETYNSYEWTKTAAHMITTBGOW - "Fragen, die häufig gestellt werden, außer dass Sie nicht sicher sind, ob Sie überhaupt die Antwort wissen wollen und wichtigere Dinge zu erledigen haben"?
Steve Jessop
4
außergewöhnliche Antwort, frage mich, ob die Frage in die FAQ passen könnte.
Matthieu M.
Whoa, können wir Enzyklopädie sagen? highfive Ein subtiler Punkt: "Wenn Foo mit einem Typ instanziiert wird, der ein Datenelement A anstelle eines verschachtelten Typs A hat, ist dies ein Fehler im Code, der die Instanziierung durchführt (Phase 2), kein Fehler in der Vorlage (Phase) 1). " Es könnte besser sein zu sagen, dass die Vorlage nicht fehlerhaft ist, aber dies könnte immer noch ein Fall einer falschen Annahme oder eines logischen Fehlers seitens des Vorlagenschreibers sein. Wenn die markierte Instanziierung tatsächlich der beabsichtigte Anwendungsfall wäre, wäre die Vorlage falsch.
Ionoclast Brigham
1
@ JohnH. Angesichts der Tatsache, dass mehrere Compiler -fpermissiveoder ähnliches implementieren , ist dies möglich. Ich kenne die Details der Implementierung nicht, aber der Compiler muss die Auflösung verschieben, xbis er die tatsächliche temporäre Basisklasse kennt T. Im nicht zulässigen Modus kann es also im Prinzip die Tatsache aufzeichnen, dass es verschoben wurde, es verschieben, die Suche durchführen, sobald dies der Fall ist T, und wenn die Suche erfolgreich ist, den von Ihnen vorgeschlagenen Text ausgeben. Es wäre ein sehr genauer Vorschlag, wenn er nur in den Fällen gemacht würde, in denen er funktioniert: Die Chancen, dass der Benutzer einen anderen xaus einem anderen Bereich meinte, sind ziemlich gering!
Steve Jessop
13

(Ursprüngliche Antwort vom 10. Januar 2011)

Ich glaube, ich habe die Antwort gefunden: GCC-Problem: Verwenden eines Mitglieds einer Basisklasse, das von einem Vorlagenargument abhängt . Die Antwort ist nicht spezifisch für gcc.


Update: Als Antwort auf mmichaels Kommentar aus dem Entwurf N3337 des C ++ 11-Standards:

14.6.2 Abhängige Namen [temp.dep]
[...]
3 Wenn bei der Definition einer Klasse oder Klassenvorlage eine Basisklasse von einem Vorlagenparameter abhängt, wird der Bereich der Basisklasse auch bei der Suche nach nicht qualifizierten Namen unter nicht untersucht der Definitionspunkt der Klassenvorlage oder des Mitglieds oder während einer Instanziierung der Klassenvorlage oder des Mitglieds.

Ob "weil der Standard es sagt" als Antwort zählt, weiß ich nicht. Wir können jetzt fragen, warum der Standard dies vorschreibt, aber wie Steve Jessops ausgezeichnete Antwort und andere hervorheben, ist die Antwort auf diese letztere Frage ziemlich lang und fraglich. Wenn es um den C ++ - Standard geht, ist es leider oft fast unmöglich, eine kurze und in sich geschlossene Erklärung zu geben, warum der Standard etwas vorschreibt. Dies gilt auch für die letztere Frage.

Ali
quelle
11

Das xist während der Vererbung versteckt. Sie können einblenden über:

template <typename T>
class derived : public base<T> {

public:
    using base<T>::x;             // added "using" statement
    int f() { return x; }
};
Chrisaycock
quelle
25
Diese Antwort erklärt nicht, warum es versteckt ist.
Jamesdlin