Zeitpunkt der Instanziierung von Templates



  • Guten Abend miteinander,

    Mich beschäftigt folgender Code:

    #include <iostream>
    
    template <typename T>
    void Func(T)
    {
    	std::cout << "T" << std::endl;
    }
    
    template <typename T>
    void Use()
    {
    	Func(T());
    }
    
    void Func(int)
    {
    	std::cout << "int" << std::endl;
    }
    
    int main()
    {
    	Use<double>();
    	Use<int>();
    }
    

    Ausgabe (MSVC++):

    T
    int
    

    Ausgabe (g++):

    T  
    T
    

    Templates werden also unterschiedlich bezüglich ihres Instanziierungszeitpunktes behandelt. Der Microsoft-Compiler führt die Instanziierung von Use() erst in main() durch, daher tritt Func(int) als zusätzlicher Kandidat in der Überladungsauflösung auf und wird schliesslich ausgewählt. Hingegen scheint g++ Use() bereits bei der Definition teilweise zu kompilieren, wo erst eine Funktion (nämlich das allgemeine Template Func(T) ) sichtbar ist.

    Was ist laut Standard-C++ richtig?



  • Soviel ich weiss, kommt Templatevervollständigung erst nach Überladung, aber ohne Gewähr.

    MfG, EOutOfResources



  • Weiss jemand Genaueres darüber?

    Für mich wäre auch interessant zu sehen, wie andere Leute mit solchen Problemen umgehen, sofern sie auftreten. Das kann ja durchaus ein recht mühsamer Bug sein...


  • Mod

    Nexus schrieb:

    Weiss jemand Genaueres darüber?

    Ich habe als du gefragt hattest mal den Standard dazu gelesen, aber nichts gefunden, was mir konkret genug für eine Antwort wäre. Ich würde tendenziell eher sagen, der MSVC hat Recht, aber es genügt ein kleiner Nebensatz den ich überlesen habe um alles wieder umzudrehen. Jedenfalls habe ich nirgendwo eine konkrete Definition gefunden, wann die Funktionen aufgelöst werden.

    Für mich wäre auch interessant zu sehen, wie andere Leute mit solchen Problemen umgehen, sofern sie auftreten. Das kann ja durchaus ein recht mühsamer Bug sein...

    Keine Ahnung. Ein Forum mit einem (noch) höherem Niveau befragen? Das ist jedoch schon eine ziemliche Gurufrage, eventuell sogar eine Schwachstelle im Standard die hier vorliegt.



  • Ich hatte gehofft, dass das mal einer nachguckt und beantwortet. 🙂 Ich selber weiß es auch nicht. Ich hatte aber mal ähnliche Probleme mit Funktionstemplates, die sich gegenseitig hätten aufrufen sollen. Ich war dann gezwungen, eines davon vorher zu deklarieren. (ui!... jeden Satz mit "Ich" angefangen *schäm*)

    Prinzipiell würde ich da dem GCC mehr vertrauen, was name lookup angeht. Der MSVC ist bekannt dafür, dass er kein richtiges 2-phasen-lookup macht, wie es eigentlich vom Standard gefordert ist. Das macht ihn einerseits gnädiger, was fehlende typename s angeht, andererseits werden andere Fehler erst später erkannt (statt in der ersten dann in der zweiten Phase, wo das Template instantiiert wird). Aber am 2-phasen-lookup kann es eigentlich auch nicht liegen. Der Knackpunkt ist hier wahrscheinlich der "POI" (point of instantiation).

    Ich habe noch das Buch "C++ Templates -- The Complete Guide" zu Hause liegen. Vielleicht finde ich darin eine passende Erklärung. Ich werd mal heute abend reingucken. Wenn ich mich richtig erinnere war da die Rede von POIs.

    Grad mal in einem Draft nach "point of instantiation" gesucht und folgende Stelle (n3225.pdf, §14.6 [temp.res], paragraph 10) gefunden:

    If a name does not depend on a template-parameter (as defined in 14.6.2), a declaration (or set of declarations) for that name shall be in scope at the point where the name appears in the template definition; the name is bound to the declaration (or declarations) found at that point and this binding is not affected by declarations that are visible at the point of instantiation. [ Example:

    void f(char);
    template<class T> void g(T t) {
      f(1);    // f(char)
      f(T(1)); // dependent
      f(t);    // dependent
      dd++;    // not dependent
               // error: declaration for dd not found
    }
    enum E { e };
    void f(E);
    double dd;
    void h() {
      g(e);   // will cause one call of f(char) followed
              // by two calls of f(E)
      g(’a’); // will cause three calls of f(char)
    }
    

    —end example ]

    Sieht für mich grad so aus, als wär würde der GCC da etwas falsch machen (compiler-bug). Allerdings ist E ein benutzerdefinierter Typ, für den ADL greift und in Nexus' Beispiel handelt es sich nur um int .



  • Vielen Dank für die Antworten, die Erklärung klingt schon mal gut.

    Eigentlich bin ich ja auf den Fehler gestossen, weil ich in einem Funktionstemplate ein STATIC_ASSERT(false) hatte, das unter MSVC nur beim Aufruf, bei g++ aber immer zu Compilerfehlern führte. Ausserdem hatte ich einen Fall, wo man als Benutzer für seine Typen eine freie Funktion bereitstellen muss, und ansonsten ein Default-Implementierung besteht. Wenn bei g++ letztere vor dem Aufruf deklariert wird, wird nur diese als Überladung berücksichtigt. Hm, vielleicht ist Code verständlicher... 😉

    // Default-Implementierung
    template <typename T>
    void UserFunction(T t);
    
    // Bibliotheksklasse
    template <typename T>
    struct MyClass
    {
        void DoSomething(T t)
        {
            // ruft bei g++ immer die Default-Implementierung auf - ausser diese
            // ist hier noch nicht sichtbar, dann normale Überladungsauflösung
            UserFunction(t);
        }
    };
    
    // Benutzerfunktion, für T = UserClass
    void UserFunction(UserClass c);
    


  • Ich bin mir hier jetzt nicht 100% sicher und hoffe ich blamier mich nicht, aber wenn ich mich recht entsinne, und http://www.c-plusplus.net/forum/81033 mir betrachte, tut der g++ dann nicht genau das richtige?

    und mit

    // Benutzerfunktion, für T = UserClass
    template <>
    void UserFunction<UserClass>(UserClass c);
    

    hättest du beim Aufruf das gewünschte Ergebnis?



  • unsicherer schrieb:

    Ich bin mir hier jetzt nicht 100% sicher und hoffe ich blamier mich nicht

    Keine Sorge, hier scheint sich niemand wirklich sicher zu sein 😉

    Geht es im verlinkten Thread nicht eher um Templatespezialisierungen? Denn das Beispiel hier hat ja damit nichts zu tun. Überhaupt mag ich Spezialisierungen von Funktionstemplates nicht und ziehe Überladungen eigentlich immer vor.

    Ich hatte früher auch immer in Erinnerung, dass zumindest von Templateparametern abhängige Ausdrücke erst bei der Instanziierung kompiliert werden (d.h. für das Lookup werden auch die dort sichtbaren Namen verwendet). Würde ja um einiges mehr Sinn machen als bei der Definition. Ich muss bei Gelegenheit mal im Standard wühlen, fürchte aber, dass das wieder in eine Interpretiererei ausarten könnte...



  • Ich hab im Hinterkopf was von "2-phase-lookup", soll heißen, dass die Namen in zwei Schritten zu unterschidelichen Zeiten nachgeschaut werden. IIRC macht das nur einer (oder garkeiner?) von beiden Compilern wirklich korrekt. Das stand vermutlich im Josuttis/Vandervoorde - hab ich aber grad nicht zur Hand, müsste zu Hause mal nachschlagen.



  • Wenn man die Reihenfolge von Template und der Ueberladung nicht mit anderen Templates mischt, dann liefert g++ auch T und int als Ausgabe.


  • Mod

    gcc macht es richtig, msvc nicht:

    14.6.2/1
    ...
    In an expression of
    the form:
    postfix-expression ( expression-listopt )
    where the postfix-expression is an identifier, the identifier denotes a dependent name if and only if any of
    the expressions in the expression-list is a type-dependent expression (14.6.2.2).

    In

    Func(T());
    

    sind sowohl T (14.6.2.1) als auch T() (14.6.2.2) type-dependent. Damit ist auch func ein abhängiger Name.

    14.6.4 Dependent name resolution [temp.dep.res]
    1 In resolving dependent names, names from the following sources are considered:
    — Declarations that are visible at the point of definition of the template.
    — Declarations from namespaces associated with the types of the function arguments both from the instantiation
    context (14.6.4.1) and from the definition context.

    Und damit ist alles klar. Bei der Definition des Templates ist nur das Funktionstemplate sichtbar. ADL im zweiten Schritt nützt auch nichts: fundamentale Typen haben keinen assoziierten Namensraum. Würde man in dem Code hingen das int durch eine entsprechende Klasse ersetzen, würde das vom OP wohl erwartete Verhalten herauskommen.



  • Vielen Dank für die nachträgliche Erklärung!

    camper schrieb:

    Würde man in dem Code hingen das int durch eine entsprechende Klasse ersetzen, würde das vom OP wohl erwartete Verhalten herauskommen.

    Das ist ja wieder mal super-unintuitiv. Ich frage mich, was man mit der Sonderbehandlung von fundamentalen Typen hier bezweckt. Gerade falls man typedef s hat, kann sich bei einer Typänderung das Programmverhalten komplett ändern, ohne dass man zur Kompilierzeit was davon mitbekommt.


  • Mod

    Habe gar nicht gemerkt, dass der Thread so alt ist. Der stand mitten zwischen den Anderen von heute. Komisch.



  • Kein Problem, im Gegenteil! Ich bin froh, dass das Ganze doch noch geklärt wurde 🙂

    Auch wenn mir persönlich die Art, wie C++ das handhabt, nicht gefällt. Aber vielleicht übersehe ich auch die Intention hinter der "Spezialbehandlung" von fundamentalen Typen...



  • Super ... ich hatte das schon wieder ganz vergessen, dass ich das nachgucken wollte. Wie gesagt, was name lookup angeht, vertraue ich eigentlich dem GCC mehr. Die Erklärungen überraschen mich also nicht. 🙂

    Danke, camper.



  • Nexus schrieb:

    Aber vielleicht übersehe ich auch die Intention hinter der "Spezialbehandlung" von fundamentalen Typen...

    Ich kann mir eigentlich nicht vorstellen, dass das eine gewollte Spezialbehandlung ist. Die Regeln sind doch so komplex, könnte sich sowas nicht einfach "ergeben"?

    Und: Findet man solche versteckten Abstrusitäten in anderen Sprachen eigentlich auch? Oder ist das eine Kombi durch die heterogenen Compiler, das lange Regelwerk (insb. durch Templates?), den sorgsam ausformulierten Standard und das relativ hohe Niveau mancher Benutzer der Sprache?
    Ich kenne mich in anderen Programmiersprachen nicht so gut aus wie in C++, aber ich versuche mir grad eine ähnliche Diskussion zu Python- oder Java-Verhalten vorzustellen.


  • Mod

    Nexus schrieb:

    Aber vielleicht übersehe ich auch die Intention hinter der "Spezialbehandlung" von fundamentalen Typen...

    Ich sehe da eigentlich keine Sonderbehandlung. Der assozierte Namensraum einer Entität ist derjenige Namensraum, in dem diese definiert wird (bzw. bei zusammengesetzten Typen, die Menge aller assozierten Namensräume, von denen sich der Typ ableitet). Fundamentale Typen können nicht definiert werden, folglich gibt es auch keinen assozierten Namensraum für sie. Was wäre denn die Alternative? der globale Namensraum? ::std? Der (überraschende Effekt) des nicht Gefundenwerdens ist ja trotzdem nicht nur auf fundamentale Typen beschränkt

    #include <iostream>
    #include <vector>
    struct Foo {};
    template <typename T> void Func(T) { std::cout << "T" << std::endl; }
    template <typename T> void Use() { Func(T()); }
    void Func(int) { std::cout << "int" << std::endl; }
    void Func(Foo) { std::cout << "Foo" << std::endl; }
    template <typename T> void Func(T*) { std::cout << "T*" << std::endl; }
    void Func(std::vector<char>*) { std::cout << "std::vector<char>*" << std::endl; }
    
    int main()
    {
        Use<double>(); // T
        Use<int>();    // T
        Use<Foo>();    // Foo
        Use<Foo*>();   // T*
        Use<std::vector<char>>();   // T   - Überladung müsste im Namensraum ::std stattfinden, was nat. nicht geht
        Use<std::vector<char>*>();  // T
        Use<std::vector<Foo>*>();  // T*
    }
    

    Man kann sicherlich über das Für und Wider des 2-Phasen-Lookups diskutieren. Damit kann man allerdings auch Bücher füllen 🙂
    Zusammenfassend: Überladung ist fast genauso böse wie Spezialisierung.


Anmelden zum Antworten