Kurze Antwort: Um x
einen 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, int
ist der obige Code falsch geformt und muss diagnostiziert werden, so wie es wäre, wenn überhaupt foo
keine 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. A
Hier 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::A
wä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 T
wahrscheinlich 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, typename
um 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::A
als Basisklasse verwendet und ist daher eindeutig ein Typ. If Foo
wird 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::string
wä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 Bar
zuvor definiert Foo
wurde und x
kein Mitglied dieser Definition ist, später eine Spezialisierung Bar
fü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 Bar
hattex
eine Spezialisierung ohne diese definieren würde und Ihre Vorlage nach einer globalen Version suchen würde, x
in 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, A
abhängig zu machen :
using Bar<T>::A;
in der Klasse - A
bezieht sich jetzt auf etwas in Bar<T>
, daher abhängig.
Bar<T>::A *x = 0;
am Point of Use - Wieder A
ist definitiv in Bar<T>
. Dies ist eine Multiplikation, da sie typename
nicht 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 - A
ist 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::string
kö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.