Wo und warum muss ich die Schlüsselwörter "Vorlage" und "Typname" eingeben?

1125

In Vorlagen, wo und warum muss ich setzen typenameund templateauf abhängige Namen?
Was genau sind abhängige Namen überhaupt?

Ich habe folgenden Code:

template <typename T, typename Tail> // Tail will be a UnionNode too.
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        // Q: where to add typename/template here?
        typedef Tail::inUnion<U> dummy; 
    };
    template< > struct inUnion<T> {
    };
};
template <typename T> // For the last node Tn.
struct UnionNode<T, void> {
    // ...
    template<typename U> struct inUnion {
        char fail[ -2 + (sizeof(U)%2) ]; // Cannot be instantiated for any U
    };
    template< > struct inUnion<T> {
    };
};

Das Problem, das ich habe, liegt in der typedef Tail::inUnion<U> dummyLeitung. Ich bin mir ziemlich sicher, dass dies inUnionein abhängiger Name ist, und VC ++ ist völlig richtig darin, daran zu ersticken.
Ich weiß auch, dass ich templateirgendwo etwas hinzufügen sollte , um dem Compiler mitzuteilen, dass inUnion eine Vorlagen-ID ist. Aber wo genau? Und sollte es dann annehmen, dass inUnion eine Klassenvorlage ist, dh inUnion<U>einen Typ und keine Funktion benennt?

MSalters
quelle
1
Ärgerliche Frage: Warum nicht Boost :: Variant?
Assaf Lavie
58
Politische Sensibilität, Portabilität.
MSalters
5
Ich habe Ihre eigentliche Frage ("Wo soll Vorlage / Typname platziert werden?") Hervorzuheben, indem ich die letzte Frage und den Code am Anfang platziert und den Code horizontal gekürzt habe, um ihn an einen 1024x-Bildschirm anzupassen.
Johannes Schaub - Litb
7
Die "abhängigen Namen" wurden aus dem Titel entfernt, da die meisten Leute, die sich über "Typname" und "Vorlage" wundern, anscheinend nicht wissen, was "abhängige Namen" sind. Auf diese Weise sollte es für sie weniger verwirrend sein.
Johannes Schaub - litb
2
@ MSalters: Boost ist ziemlich portabel. Ich würde sagen, nur die Politik ist der allgemeine Grund, warum der Schub oft nicht berücksichtigt wird. Der einzige gute Grund, den ich kenne, sind die verlängerten Bauzeiten. Ansonsten geht es darum, Tausende von Dollar zu verlieren, um das Rad neu zu erfinden.
v.oddou

Antworten:

1163

(Siehe hier auch für meine C ++ 11 Antwort )

Um ein C ++ - Programm zu analysieren, muss der Compiler wissen, ob bestimmte Namen Typen sind oder nicht. Das folgende Beispiel zeigt Folgendes:

t * f;

Wie soll das analysiert werden? Für viele Sprachen muss ein Compiler die Bedeutung eines Namens nicht kennen, um zu analysieren und im Grunde zu wissen, welche Aktion eine Codezeile ausführt. In C ++ kann das oben Genannte jedoch zu sehr unterschiedlichen Interpretationen führen, je nachdem, was tbedeutet. Wenn es sich um einen Typ handelt, handelt es sich um eine Deklaration eines Zeigers f. Wenn es sich jedoch nicht um einen Typ handelt, handelt es sich um eine Multiplikation. Der C ++ Standard sagt also in Absatz (3/7):

Einige Namen bezeichnen Typen oder Vorlagen. Im Allgemeinen muss bei jedem Auftreten eines Namens festgestellt werden, ob dieser Name eine dieser Entitäten bezeichnet, bevor das darin enthaltene Programm weiter analysiert wird. Der Prozess, der dies feststellt, wird als Namenssuche bezeichnet.

Wie findet der Compiler heraus, worauf sich ein Name t::xbezieht, wenn er tsich auf einen Vorlagentypparameter bezieht? xkönnte ein statisches int-Datenelement sein, das multipliziert werden könnte, oder könnte ebenso gut eine verschachtelte Klasse oder ein typedef sein, das einer Deklaration nachgeben könnte. Wenn ein Name diese Eigenschaft hat - dass er erst nachgeschlagen werden kann, wenn die tatsächlichen Vorlagenargumente bekannt sind -, wird er als abhängiger Name bezeichnet (dies hängt von den Vorlagenparametern ab).

Sie können empfehlen, nur zu warten, bis der Benutzer die Vorlage instanziiert:

Warten wir, bis der Benutzer die Vorlage instanziiert, und finden Sie später die wahre Bedeutung von heraus t::x * f;.

Dies wird funktionieren und wird vom Standard als möglicher Implementierungsansatz tatsächlich zugelassen. Diese Compiler kopieren den Text der Vorlage grundsätzlich in einen internen Puffer. Nur wenn eine Instanziierung erforderlich ist, analysieren sie die Vorlage und erkennen möglicherweise Fehler in der Definition. Anstatt die Benutzer der Vorlage (arme Kollegen!) Mit Fehlern des Autors einer Vorlage zu belästigen, prüfen andere Implementierungen die Vorlagen frühzeitig und geben so schnell wie möglich Fehler in der Definition an, bevor überhaupt eine Instanziierung stattfindet.

Es muss also eine Möglichkeit geben, dem Compiler mitzuteilen, dass bestimmte Namen Typen sind und bestimmte Namen nicht.

Das Schlüsselwort "Typname"

Die Antwort lautet: Wir entscheiden, wie der Compiler dies analysieren soll. Wenn t::xes sich um einen abhängigen Namen handelt, müssen wir ihm ein Präfix voranstellen, damit typenameder Compiler ihn auf eine bestimmte Weise analysieren kann. Der Standard sagt bei (14.6 / 2):

Es wird angenommen, dass ein Name, der in einer Vorlagendeklaration oder -definition verwendet wird und von einem Vorlagenparameter abhängig ist, keinen Typ benennt, es sei denn, die entsprechende Namenssuche findet einen Typnamen oder der Name wird durch das Schlüsselwort Typname qualifiziert.

Es gibt viele Namen, für die dies typenamenicht erforderlich ist, da der Compiler mit der entsprechenden Namenssuche in der Vorlagendefinition herausfinden kann, wie ein Konstrukt selbst analysiert wird - beispielsweise mit T *f;, wenn Tes sich um einen Typvorlagenparameter handelt. Aber t::x * f;um eine Erklärung zu sein, muss sie wie folgt geschrieben sein typename t::x *f;. Wenn Sie das Schlüsselwort weglassen und der Name als Nicht-Typ angesehen wird, die Instanziierung jedoch feststellt, dass es sich um einen Typ handelt, werden die üblichen Fehlermeldungen vom Compiler ausgegeben. Manchmal wird der Fehler folglich zur Definitionszeit angegeben:

// t::x is taken as non-type, but as an expression the following misses an
// operator between the two names or a semicolon separating them.
t::x f;

Die Syntax erlaubt typenamenur vor qualifizierten Namen - es wird daher als selbstverständlich angesehen, dass nicht qualifizierte Namen immer bekanntermaßen auf Typen verweisen, wenn sie dies tun.

Ein ähnliches Gotcha gibt es für Namen, die Vorlagen bezeichnen, wie im Einführungstext angedeutet.

Das Schlüsselwort "Vorlage"

Erinnern Sie sich an das erste Zitat oben und wie der Standard auch für Vorlagen eine spezielle Behandlung erfordert? Nehmen wir das folgende unschuldig aussehende Beispiel:

boost::function< int() > f;

Für einen menschlichen Leser mag es offensichtlich erscheinen. Nicht so beim Compiler. Stellen Sie sich die folgende willkürliche Definition von boost::functionund vor f:

namespace boost { int function = 0; }
int main() { 
  int f = 0;
  boost::function< int() > f; 
}

Das ist eigentlich ein gültiger Ausdruck ! Es verwendet den Operator kleiner als zum Vergleichen boost::functionmit Null ( int()) und verwendet dann den Operator größer als zum Vergleichen des Ergebnisses boolmit f. Wie Sie vielleicht wissen, handelt es sich boost::function im wirklichen Leben um eine Vorlage, sodass der Compiler weiß (14.2 / 3):

Nachdem die Namenssuche (3.4) festgestellt hat, dass ein Name ein Vorlagenname ist. Wenn auf diesen Namen ein <folgt, wird das <immer als Anfang einer Vorlagenargumentliste und niemals als Name gefolgt von dem weniger- verwendet. als Betreiber.

Jetzt sind wir wieder bei dem gleichen Problem wie bei typename. Was ist, wenn wir beim Parsen des Codes noch nicht wissen können, ob der Name eine Vorlage ist? Wir müssen templateunmittelbar vor dem Vorlagennamen einfügen , wie von angegeben 14.2/4. Das sieht so aus:

t::template f<int>(); // call a function template

Vorlagennamen können nicht nur nach einem, ::sondern auch nach einem ->oder .in einem Klassenmitgliedszugriff auftreten. Dort müssen Sie auch das Schlüsselwort einfügen:

this->template f<int>(); // call a function template

Abhängigkeiten

Für die Leute, die dicke Standardese-Bücher in ihrem Regal haben und wissen wollen, worüber ich genau gesprochen habe, werde ich ein wenig darüber sprechen, wie dies im Standard festgelegt ist.

In Vorlagendeklarationen haben einige Konstrukte unterschiedliche Bedeutungen, je nachdem, welche Vorlagenargumente Sie zum Instanziieren der Vorlage verwenden: Ausdrücke können unterschiedliche Typen oder Werte haben, Variablen können unterschiedliche Typen haben oder Funktionsaufrufe können unterschiedliche Funktionen aufrufen. Solche Konstrukte sollen im Allgemeinen von Schablonenparametern abhängen .

Der Standard definiert genau die Regeln, ob ein Konstrukt abhängig ist oder nicht. Es unterteilt sie in logisch unterschiedliche Gruppen: Einer fängt Typen ab, ein anderer fängt Ausdrücke ab. Ausdrücke können von ihrem Wert und / oder ihrem Typ abhängen. Wir haben also mit typischen Beispielen Folgendes angehängt:

  • Abhängige Typen (z. B. ein Typvorlagenparameter T)
  • Wertabhängige Ausdrücke (z. B. ein nicht typisierter Vorlagenparameter N)
  • Typabhängige Ausdrücke (z. B. Umwandlung in einen Typvorlagenparameter (T)0)

Die meisten Regeln sind intuitiv und rekursiv aufgebaut: Beispielsweise ist ein Typ, der als T[N]abhängiger Typ konstruiert ist, ein Nwertabhängiger Ausdruck oder Tein abhängiger Typ. Die Details hierzu finden Sie im Abschnitt (14.6.2/1) für abhängige Typen, (14.6.2.2)für typabhängige Ausdrücke und (14.6.2.3)für wertabhängige Ausdrücke.

Abhängige Namen

Der Standard ist etwas unklar, was genau ein abhängiger Name ist . Bei einem einfachen Lesevorgang (Sie wissen, das Prinzip der geringsten Überraschung) ist alles, was als abhängiger Name definiert wird, der Sonderfall für Funktionsnamen unten. Da T::xdies natürlich auch im Instanziierungskontext nachgeschlagen werden muss, muss es sich auch um einen abhängigen Namen handeln (glücklicherweise hat das Komitee ab Mitte C ++ 14 begonnen, zu prüfen, wie diese verwirrende Definition behoben werden kann).

Um dieses Problem zu vermeiden, habe ich auf eine einfache Interpretation des Standardtextes zurückgegriffen. Von allen Konstrukten, die abhängige Typen oder Ausdrücke bezeichnen, repräsentiert eine Teilmenge Namen. Diese Namen sind daher "abhängige Namen". Ein Name kann verschiedene Formen annehmen - der Standard sagt:

Ein Name ist eine Verwendung eines Bezeichners (2.11), einer Operatorfunktions-ID (13.5), einer Konvertierungsfunktions-ID (12.3.2) oder einer Vorlagen-ID (14.2), die eine Entität oder Bezeichnung (6.6.4, 6.1)

Ein Bezeichner ist nur eine einfache Folge von Zeichen / Ziffern, während die nächsten beiden die operator +und operator type-Form sind. Die letzte Form ist template-name <argument list>. All dies sind Namen, und bei herkömmlicher Verwendung im Standard kann ein Name auch Qualifizierer enthalten, die angeben, in welchem ​​Namespace oder in welcher Klasse ein Name gesucht werden soll.

Ein wertabhängiger Ausdruck 1 + Nist kein Name, sondern ein Name N. Die Teilmenge aller abhängigen Konstrukte, bei denen es sich um Namen handelt, wird als abhängiger Name bezeichnet . Funktionsnamen können jedoch in verschiedenen Instanziierungen einer Vorlage unterschiedliche Bedeutungen haben, werden jedoch von dieser allgemeinen Regel leider nicht erfasst.

Abhängige Funktionsnamen

Nicht in erster Linie ein Anliegen dieses Artikels, aber dennoch erwähnenswert: Funktionsnamen sind eine Ausnahme, die separat behandelt werden. Der Name einer Bezeichnerfunktion hängt nicht von sich selbst ab, sondern von den typabhängigen Argumentausdrücken, die in einem Aufruf verwendet werden. In dem Beispiel f((T)0), fist ein abhängiger Name. Im Standard ist dies unter angegeben (14.6.2/1).

Zusätzliche Hinweise und Beispiele

In genügend Fällen brauchen wir sowohl von typenameals auch template. Ihr Code sollte wie folgt aussehen

template <typename T, typename Tail>
struct UnionNode : public Tail {
    // ...
    template<typename U> struct inUnion {
        typedef typename Tail::template inUnion<U> dummy;
    };
    // ...
};

Das Schlüsselwort templatemuss nicht immer im letzten Teil eines Namens stehen. Es kann in der Mitte vor einem Klassennamen erscheinen, der wie im folgenden Beispiel als Bereich verwendet wird

typename t::template iterator<int>::value_type v;

In einigen Fällen sind die Schlüsselwörter verboten, wie unten beschrieben

  • Auf den Namen einer abhängigen Basisklasse dürfen Sie nicht schreiben typename. Es wird angenommen, dass der angegebene Name ein Klassentypname ist. Dies gilt sowohl für Namen in der Basisklassenliste als auch in der Konstruktorinitialisiererliste:

     template <typename T>
     struct derive_from_Has_type : /* typename */ SomeBase<T>::type 
     { };
  • In using-Deklarationen ist es nicht möglich, templatenach dem letzten zu verwenden ::, und das C ++ - Komitee sagte, dass es nicht an einer Lösung arbeiten soll.

     template <typename T>
     struct derive_from_Has_type : SomeBase<T> {
        using SomeBase<T>::template type; // error
        using typename SomeBase<T>::type; // typename *is* allowed
     };
Johannes Schaub - litb
quelle
22
Diese Antwort wurde aus meinem früheren FAQ-Eintrag kopiert, den ich entfernt habe, da ich festgestellt habe, dass ich vorhandene ähnliche Fragen besser verwenden sollte, anstatt neue "Pseudo-Fragen" zu erstellen, nur um sie zu beantworten. Vielen Dank an @Prasoon , der die Ideen des letzten Teils (Fälle, in denen Typname / Vorlage verboten ist) in der Antwort bearbeitet hat.
Johannes Schaub - Litb
1
Können Sie mir helfen, wann ich diese Syntax verwenden soll? this-> template f <int> (); Ich erhalte den Fehler 'Vorlage' (als Disambiguator) ist nur in Vorlagen zulässig, aber ohne das Schlüsselwort template funktioniert es einwandfrei.
Balki
1
Ich habe heute eine ähnliche Frage gestellt, die bald als Duplikat markiert wurde: stackoverflow.com/questions/27923722/… . Ich wurde angewiesen, diese Frage wiederzubeleben, anstatt eine neue zu erstellen. Ich muss sagen, ich bin nicht damit einverstanden, dass es sich um Duplikate handelt, aber wer bin ich, richtig? Gibt es also einen Grund, der typenameerzwungen wird, selbst wenn die Syntax zu diesem Zeitpunkt keine anderen alternativen Interpretationen als Typnamen zulässt?
JorenHeit
1
@Pablo dir fehlt nichts. Aber immer noch erforderlich, um die Begriffsklärung zu schreiben, auch wenn die gesamte Zeile nicht mehr mehrdeutig wäre.
Johannes Schaub - litb
1
@Pablo Der Zweck ist es, die Sprache und die Compiler einfacher zu halten. Es gibt Vorschläge, mit denen mehr Situationen automatisch herausfinden können, sodass Sie das Schlüsselwort weniger häufig benötigen. Beachten Sie, dass in Ihrem Beispiel, das Token ist mehrdeutig und erst , nachdem Sie gesehen haben „>“ nach einem Doppelklick können Sie es als Vorlage Winkel eindeutig machen. Für weitere Details bin ich die falsche Person, da ich keine Erfahrung mit der Implementierung eines Parsers für C ++ - Compiler habe.
Johannes Schaub - litb
135

C ++ 11

Problem

Während die Regeln in C ++ 03 festlegen, wann Sie sie benötigen typenameund templateweitgehend vernünftig sind, gibt es einen ärgerlichen Nachteil bei der Formulierung

template<typename T>
struct A {
  typedef int result_type;

  void f() {
    // error, "this" is dependent, "template" keyword needed
    this->g<float>();

    // OK
    g<float>();

    // error, "A<T>" is dependent, "typename" keyword needed
    A<T>::result_type n1;

    // OK
    result_type n2; 
  }

  template<typename U>
  void g();
};

Wie zu sehen ist, benötigen wir das Schlüsselwort "Disambiguierung", selbst wenn der Compiler sich selbst perfekt herausfinden könnte, dass A::result_typedies nur sein kann int(und daher ein Typ ist) und this->gnur die gspäter deklarierte Mitgliedsvorlage sein kann (selbst wenn dies Airgendwo explizit spezialisiert ist) den Code in dieser Vorlage nicht beeinflussen, daher kann seine Bedeutung durch eine spätere Spezialisierung von A!) nicht beeinflusst werden.

Aktuelle Instanziierung

Um die Situation zu verbessern, verfolgt die Sprache in C ++ 11, wenn sich ein Typ auf die einschließende Vorlage bezieht. Um zu wissen , dass, muss der Typ unter Verwendung eine bestimmte Form des Namen gebildet worden war, die ihr eigener Name ist (in den oben genannten, A, A<T>, ::A<T>). Ein Typ, auf den ein solcher Name verweist, ist als aktuelle Instanziierung bekannt . Es kann mehrere Typen geben, die die gesamte aktuelle Instanziierung darstellen, wenn der Typ, aus dem der Name gebildet wird, ein Mitglied / eine verschachtelte Klasse ist (dann A::NestedClassund Abeide aktuelle Instanziierungen).

Basierend auf dieser Idee, sagt die Sprache , das CurrentInstantiation::Foo, Foound CurrentInstantiationTyped->Foo(wie A *a = this; a->Foo) sind alle Mitglieder der aktuellen Instanziierung , wenn sie gefunden werden , Mitglieder einer Klasse zu sein , dass die aktuelle Instantiierung ist oder eines seiner Nicht-abhängigen Basisklassen (von nur tun , die Namenssuche sofort).

Die Schlüsselwörter typenameund templatewerden jetzt nicht mehr benötigt, wenn das Qualifikationsmerkmal Mitglied der aktuellen Instanziierung ist. Ein wichtiger Punkt hierbei ist, dass A<T>es sich immer noch um einen typabhängigen Namen handelt (schließlich Tist er auch typabhängig). Es A<T>::result_typeist jedoch bekannt, dass es sich um einen Typ handelt - der Compiler wird diese Art von abhängigen Typen "magisch" untersuchen, um dies herauszufinden.

struct B {
  typedef int result_type;
};

template<typename T>
struct C { }; // could be specialized!

template<typename T>
struct D : B, C<T> {
  void f() {
    // OK, member of current instantiation!
    // A::result_type is not dependent: int
    D::result_type r1;

    // error, not a member of the current instantiation
    D::questionable_type r2;

    // OK for now - relying on C<T> to provide it
    // But not a member of the current instantiation
    typename D::questionable_type r3;        
  }
};

Das ist beeindruckend, aber können wir es besser machen? Die Sprache geht sogar noch weiter und erfordert, dass eine Implementierung D::result_typebeim Instanziieren erneut nachgeschlagen wird D::f(auch wenn sie ihre Bedeutung bereits zum Zeitpunkt der Definition gefunden hat). Wenn sich das Suchergebnis jetzt unterscheidet oder zu Mehrdeutigkeiten führt, ist das Programm fehlerhaft und es muss eine Diagnose gestellt werden. Stellen Sie sich vor was passiert , wenn wir definiert geschieht Cwie folgt aus

template<>
struct C<int> {
  typedef bool result_type;
  typedef int questionable_type;
};

Ein Compiler ist erforderlich, um den Fehler beim Instanziieren abzufangen D<int>::f. So erhalten Sie das Beste aus zwei Welten: "Verzögerte" Suche, die Sie schützt, wenn Sie Probleme mit abhängigen Basisklassen bekommen könnten, und "Sofortige" Suche, die Sie von typenameund befreit template.

Unbekannte Spezialisierungen

Im Code von Dist der Name typename D::questionable_typekein Mitglied der aktuellen Instanziierung. Stattdessen kennzeichnet die Sprache es als Mitglied einer unbekannten Spezialisierung . Dies ist insbesondere dann immer der Fall, wenn Sie dies tun, DependentTypeName::Foooder DependentTypedName->Fooentweder ist der abhängige Typ nicht die aktuelle Instanziierung (in diesem Fall kann der Compiler aufgeben und sagen "Wir werden später nachsehen, was Fooist") oder es ist die aktuelle Instanziierung und die Der Name wurde darin oder in seinen nicht abhängigen Basisklassen nicht gefunden, und es gibt auch abhängige Basisklassen.

Stellen Sie sich vor, was passiert, wenn wir eine Mitgliedsfunktion hin der oben definierten AKlassenvorlage haben

void h() {
  typename A<T>::questionable_type x;
}

In C ++ 03 konnte die Sprache diesen Fehler abfangen, da es niemals eine gültige Möglichkeit zum Instanziieren geben konnte A<T>::h(unabhängig davon, welches Argument Sie angeben T). In C ++ 11 hat die Sprache jetzt eine weitere Prüfung, um den Compilern mehr Gründe für die Implementierung dieser Regel zu geben. Da Akeine abhängigen Basisklassen hat, und Aerklärt , kein Mitglied questionable_typeder Name A<T>::questionable_typeist weder ein Mitglied der aktuellen Instanziierung nochein Mitglied einer unbekannten Spezialisierung. In diesem Fall sollte es keine Möglichkeit geben, dass dieser Code zum Zeitpunkt der Instanziierung gültig kompiliert werden kann. Daher verbietet die Sprache, dass ein Name, bei dem das Qualifikationsmerkmal die aktuelle Instanziierung ist, weder Mitglied einer unbekannten Spezialisierung noch Mitglied der aktuellen Instanziierung ist (jedoch) muss dieser Verstoß noch nicht diagnostiziert werden).

Beispiele und Wissenswertes

Sie können dieses Wissen anhand dieser Antwort ausprobieren und anhand eines Beispiels aus der Praxis feststellen, ob die obigen Definitionen für Sie sinnvoll sind (sie werden in dieser Antwort etwas weniger detailliert wiederholt).

Die C ++ 11-Regeln machen den folgenden gültigen C ++ 03-Code falsch (was nicht vom C ++ - Komitee beabsichtigt war, aber wahrscheinlich nicht behoben wird)

struct B { void f(); };
struct A : virtual B { void f(); };

template<typename T>
struct C : virtual B, T {
  void g() { this->f(); }
};

int main() { 
  C<A> c; c.g(); 
}

Dieser gültige C ++ 03-Code wird this->fzum A::fZeitpunkt der Instanziierung gebunden und alles ist in Ordnung. C ++ 11 bindet es jedoch sofort an B::fund erfordert beim Instanziieren eine doppelte Überprüfung, um zu überprüfen, ob die Suche noch übereinstimmt. Beim Instanziieren C<A>::ggilt jedoch die Dominanzregel , und A::fstattdessen wird nachgeschlagen .

Johannes Schaub - litb
quelle
fyi - auf diese Antwort wird hier verwiesen : stackoverflow.com/questions/56411114/… Ein Großteil des Codes in dieser Antwort wird nicht auf verschiedenen Compilern kompiliert.
Adam Rackis
@AdamRackis unter der Annahme, dass sich die C ++ - Spezifikation seit 2013 nicht geändert hat (Datum, an dem ich diese Antwort geschrieben habe), implementieren die Compiler, mit denen Sie Ihren Code ausprobiert haben, diese C ++ 11 + -Funktion einfach noch nicht.
Johannes Schaub - litb
99

VORWORT

Dieser Beitrag soll eine einfach zu lesende Alternative zu Litbs Beitrag sein .

Der zugrunde liegende Zweck ist der gleiche; eine Erklärung zu "Wann?" und warum?" typenameund templatemuss angewendet werden.


Was ist der Zweck von typenameund template?

typenameund templatekönnen unter anderen Umständen als beim Deklarieren einer Vorlage verwendet werden.

Es gibt bestimmte Kontexte in C ++, in denen dem Compiler explizit mitgeteilt werden muss, wie ein Name zu behandeln ist, und alle diese Kontexte haben eines gemeinsam. Sie hängen von mindestens einem Template-Parameter ab .

Wir beziehen uns auf solche Namen, bei denen es zu Mehrdeutigkeiten bei der Interpretation kommen kann, wie: " abhängige Namen ".

Dieser Beitrag bietet eine Erklärung für die Beziehung zwischen abhängigen Namen und den beiden Schlüsselwörtern.


Ein SNIPPET SAGT MEHR ALS 1000 WÖRTER

Versuchen Sie, sich selbst, einem Freund oder vielleicht Ihrer Katze zu erklären, was in der folgenden Funktionsvorlage vor sich geht. Was passiert in der mit ( A ) gekennzeichneten Aussage ?

template<class T> void f_tmpl () { T::foo * x; /* <-- (A) */ }


Es ist möglicherweise nicht so einfach wie man denkt, insbesondere hängt das Ergebnis der Auswertung von ( A ) stark von der Definition des als Template-Parameter übergebenen Typs ab T.

Verschiedene Ts können die Semantik drastisch verändern.

struct X { typedef int       foo;       }; /* (C) --> */ f_tmpl<X> ();
struct Y { static  int const foo = 123; }; /* (D) --> */ f_tmpl<Y> ();


Die zwei verschiedenen Szenarien :

  • Wenn wir die Funktionsvorlage wie in ( C ) mit Typ X instanziieren , haben wir eine Deklaration eines Zeigers auf int mit dem Namen x , aber;

  • Wenn wir die Vorlage wie in ( D ) mit dem Typ Y instanziieren , würde ( A ) stattdessen aus einem Ausdruck bestehen, der das Produkt von 123 multipliziert mit einer bereits deklarierten Variablen x berechnet .



DAS GRUNDPRINZIP

Der C ++ Standard kümmert sich zumindest in diesem Fall um unsere Sicherheit und unser Wohlbefinden.

Um eine Umsetzung zu verhindern , dass potenziell vor bösen Überraschungen leiden, die Standard - Mandate , dass wir aussortieren die Mehrdeutigkeit eines abhängigen Namen durch ausdrücklich die Absicht überall besagen , möchten wir den Namen entweder als zur Behandlung von Typ-Namen oder einem template- id .

Wenn nichts angegeben ist, wird der abhängige Name entweder als Variable oder als Funktion betrachtet.



WIE BEHINDEREN SIE ABHÄNGIGE NAMEN ?

Wenn dies ein Hollywood-Film wäre, wären abhängige Namen die Krankheit, die sich durch Körperkontakt ausbreitet und sofort den Wirt betrifft, um ihn zu verwirren. Verwirrung, die möglicherweise zu einem schlecht geformten perso-, erhm .. Programm führen könnte.

Ein abhängiger Name ist ein Name, der direkt oder indirekt von einem Vorlagenparameter abhängt .

template<class T> void g_tmpl () {
   SomeTrait<T>::type                   foo; // (E), ill-formed
   SomeTrait<T>::NestedTrait<int>::type bar; // (F), ill-formed
   foo.data<int> ();                         // (G), ill-formed    
}

Wir haben vier abhängige Namen im obigen Snippet:

  • E )
    • "Typ" hängt von der Instanziierung von ab SomeTrait<T>, einschließlich Tund;
  • F )
    • "NestedTrait" , eine Vorlagen-ID , hängt von SomeTrait<T>und ab.
    • "Typ" am Ende von ( F ) hängt von NestedTrait ab , was von SomeTrait<T>und abhängt ;
  • G )
    • "data" , das wie eine Elementfunktionsvorlage aussieht , ist indirekt ein abhängiger Name, da der Typ von foo von der Instanziierung von abhängt SomeTrait<T>.

Keine der Anweisungen ( E ), ( F ) oder ( G ) ist gültig, wenn der Compiler die abhängigen Namen als Variablen / Funktionen interpretieren würde (was, wie bereits erwähnt, passiert, wenn wir nicht ausdrücklich etwas anderes sagen).

DIE LÖSUNG

Um g_tmpleine gültige Definition zu erhalten, müssen wir dem Compiler explizit mitteilen, dass wir einen Typ in ( E ), eine Vorlagen-ID und einen Typ in ( F ) sowie eine Vorlagen-ID in ( G ) erwarten .

template<class T> void g_tmpl () {
   typename SomeTrait<T>::type foo;                            // (G), legal
   typename SomeTrait<T>::template NestedTrait<int>::type bar; // (H), legal
   foo.template data<int> ();                                  // (I), legal
}

Jedes Mal, wenn ein Name einen Typ bezeichnet, müssen alle beteiligten Namen entweder Typnamen oder Namespaces sein . In diesem Sinne ist es leicht zu erkennen, dass wir uns typenameam Anfang unseres vollqualifizierten Namens bewerben .

templateDies ist jedoch in dieser Hinsicht anders, da es keine Möglichkeit gibt, zu einer Schlussfolgerung zu gelangen, wie z. "Oh, das ist eine Vorlage, dann muss diese andere Sache auch eine Vorlage sein" . Dies bedeutet, dass wir uns templatedirekt vor jedem Namen bewerben , den wir als solchen behandeln möchten.



KANN ICH NUR DIE SCHLÜSSELWÖRTER VOR JEDEM NAMEN STEHEN ?

Kann ich nur Stick typenameund templatevor jedem Namen will ich nicht zu Sorgen über den Kontext , in dem sie erscheinen ...? “ -Some C++ Developer

Die Regeln im Standard besagen, dass Sie die Schlüsselwörter anwenden dürfen, solange Sie sich mit einem qualifizierten Namen ( K ) befassen. Wenn der Name jedoch nicht qualifiziert ist, ist die Anwendung fehlerhaft ( L ).

namespace N {
  template<class T>
  struct X { };
}

         N::         X<int> a; // ...  legal
typename N::template X<int> b; // (K), legal
typename template    X<int> c; // (L), ill-formed

Hinweis : Die Anwendung typenameoder templatein einem Kontext, in dem dies nicht erforderlich ist, wird nicht als bewährte Methode angesehen. Nur weil du etwas tun kannst, heißt das nicht, dass du es tun solltest.


Zusätzlich gibt es Kontexte , in denen typenameund templatesind ausdrücklich nicht zugelassen:

  • Bei der Angabe der Basen, von denen eine Klasse erbt

    Jeder Name in einer abgeleiteten Klasse eine schriftliche Basis-Bezeichner-Liste bereits als behandelt Typname , explizite Angabe typenameist sowohl schlecht ausgebildet und redundant.

                       // .------- the base-specifier-list
     template<class T> // v
     struct Derived      : typename SomeTrait<T>::type /* <- ill-formed */ {
       ...
     };


  • Wenn die Vorlagen-ID diejenige ist, auf die in der using-Direktive einer abgeleiteten Klasse verwiesen wird

     struct Base {
       template<class T>
       struct type { };
     };
    
     struct Derived : Base {
       using Base::template type; // ill-formed
       using Base::type;          // legal
     };
Filip Roséen - refp
quelle
20
typedef typename Tail::inUnion<U> dummy;

Ich bin mir jedoch nicht sicher, ob die Implementierung von inUnion korrekt ist. Wenn ich das richtig verstehe, sollte diese Klasse nicht instanziiert werden, daher wird die Registerkarte "Fail" niemals automatisch fehlschlagen. Vielleicht ist es besser anzugeben, ob der Typ in der Union ist oder nicht, mit einem einfachen booleschen Wert.

template <typename T, typename TypeList> struct Contains;

template <typename T, typename Head, typename Tail>
struct Contains<T, UnionNode<Head, Tail> >
{
    enum { result = Contains<T, Tail>::result };
};

template <typename T, typename Tail>
struct Contains<T, UnionNode<T, Tail> >
{
    enum { result = true };
};

template <typename T>
struct Contains<T, void>
{
    enum { result = false };
};

PS: Schauen Sie sich Boost :: Variant an

PS2: Sehen Sie sich Typelisten an , insbesondere in Andrei Alexandrescus Buch: Modernes C ++ - Design

Luc Touraille
quelle
inUnion <U> würde instanziiert, wenn Sie beispielsweise versuchen würden, Union <float, bool> :: operator = (U) mit U == int aufzurufen. Es ruft eine private Menge auf (U, inUnion <U> * = 0).
MSalters
Und die Arbeit mit result = true / false ist, dass ich boost :: enable_if <> benötigen würde, was mit unserer aktuellen OSX-Toolchain nicht kompatibel ist. Die separate Vorlage ist jedoch immer noch eine gute Idee.
MSalters
Luc bedeutet den typedef Tail :: inUnion <U> Dummy; Linie. das wird Schwanz instanziieren. aber nicht inUnion <U>. Es wird instanziiert, wenn es die vollständige Definition benötigt. Dies geschieht beispielsweise, wenn Sie die Größe von annehmen oder auf ein Mitglied zugreifen (mit :: foo). @ MSalters sowieso, Sie haben ein anderes Problem:
Johannes Schaub - litb
-sizeof (U) ist niemals negativ :), da size_t ein vorzeichenloser Integer-Typ ist. Sie werden eine sehr hohe Anzahl erhalten. Sie möchten wahrscheinlich sizeof (U)> = 1 machen? -1: 1 oder ähnlich :)
Johannes Schaub - litb
Ich würde es einfach undefiniert lassen und nur deklarieren: template <typename U> struct inUnion; es kann also sicherlich nicht instanziiert werden. Ich denke, wenn man es mit der Größe von hat, darf der Compiler Ihnen auch dann einen Fehler geben, wenn Sie ihn nicht instanziieren, denn wenn Sie wissen, dass Größe von (U) immer> = 1 ist und ...
Johannes Schaub - litb
20

Diese Antwort soll eher kurz und bündig sein, um (einen Teil) der betitelten Frage zu beantworten. Wenn Sie eine ausführlichere Antwort wünschen, die erklärt, warum Sie sie dort ablegen müssen, klicken Sie bitte hier .


Die allgemeine Regel für das Einfügen des typenameSchlüsselworts lautet meistens, wenn Sie einen Vorlagenparameter verwenden und auf einen verschachtelten typedefoder verwendenden Alias zugreifen möchten , zum Beispiel:

template<typename T>
struct test {
    using type = T; // no typename required
    using underlying_type = typename T::type // typename required
};

Beachten Sie, dass dies auch für Metafunktionen oder Dinge gilt, die generische Vorlagenparameter verwenden. Wenn es sich bei dem angegebenen Vorlagenparameter jedoch um einen expliziten Typ handelt, müssen Sie beispielsweise Folgendes nicht angeben typename:

template<typename T>
struct test {
    // typename required
    using type = typename std::conditional<true, const T&, T&&>::type;
    // no typename required
    using integer = std::conditional<true, int, float>::type;
};

Die allgemeinen Regeln zum Hinzufügen des templateQualifizierers sind größtenteils ähnlich, außer dass sie normalerweise Vorlagenfunktionen (statisch oder anderweitig) einer Struktur / Klasse beinhalten, die selbst als Vorlage dient, zum Beispiel:

Angesichts dieser Struktur und Funktion:

template<typename T>
struct test {
    template<typename U>
    void get() const {
        std::cout << "get\n";
    }
};

template<typename T>
void func(const test<T>& t) {
    t.get<int>(); // error
}

Der Versuch, t.get<int>()von innerhalb der Funktion zuzugreifen, führt zu einem Fehler:

main.cpp:13:11: error: expected primary-expression before 'int'
     t.get<int>();
           ^
main.cpp:13:11: error: expected ';' before 'int'

In diesem Zusammenhang benötigen Sie also das templateSchlüsselwort im Voraus und nennen es folgendermaßen:

t.template get<int>()

Auf diese Weise analysiert der Compiler dies nicht richtig t.get < int.

Rapptz
quelle
2

Ich stelle JLBorges 'ausgezeichnete Antwort auf eine ähnliche Frage wörtlich von cplusplus.com, da dies die prägnanteste Erklärung ist, die ich zu diesem Thema gelesen habe.

In einer Vorlage, die wir schreiben, können zwei Arten von Namen verwendet werden - abhängige Namen und nicht abhängige Namen. Ein abhängiger Name ist ein Name, der von einem Vorlagenparameter abhängt. Ein nicht abhängiger Name hat unabhängig von den Vorlagenparametern dieselbe Bedeutung.

Zum Beispiel:

template< typename T > void foo( T& x, std::string str, int count )
{
    // these names are looked up during the second phase
    // when foo is instantiated and the type T is known
    x.size(); // dependant name (non-type)
    T::instance_count ; // dependant name (non-type)
    typename T::iterator i ; // dependant name (type)

    // during the first phase, 
    // T::instance_count is treated as a non-type (this is the default)
    // the typename keyword specifies that T::iterator is to be treated as a type.

    // these names are looked up during the first phase
    std::string::size_type s ; // non-dependant name (type)
    std::string::npos ; // non-dependant name (non-type)
    str.empty() ; // non-dependant name (non-type)
    count ; // non-dependant name (non-type)
}

Worauf sich ein abhängiger Name bezieht, kann für jede unterschiedliche Instanziierung der Vorlage etwas anderes sein. Infolgedessen unterliegen C ++ - Vorlagen einer "zweiphasigen Namenssuche". Wenn eine Vorlage zum ersten Mal analysiert wird (bevor eine Instanziierung stattfindet), sucht der Compiler nach den nicht abhängigen Namen. Wenn eine bestimmte Instanziierung der Vorlage stattfindet, sind die Vorlagenparameter bis dahin bekannt, und der Compiler sucht nach abhängigen Namen.

In der ersten Phase muss der Parser wissen, ob ein abhängiger Name der Name eines Typs oder der Name eines Nicht-Typs ist. Standardmäßig wird angenommen, dass ein abhängiger Name der Name eines Nicht-Typs ist. Das Schlüsselwort typename vor einem abhängigen Namen gibt an, dass es sich um den Namen eines Typs handelt.


Zusammenfassung

Verwenden Sie das Schlüsselwort Typname nur in Vorlagendeklarationen und -definitionen, sofern Sie einen qualifizierten Namen haben, der auf einen Typ verweist und von einem Vorlagenparameter abhängt.

Nikos
quelle