Fehler im C++ International Standard?


  • Mod

    n1905 ist nicht der C++ Standard sondern ein veralteter Entwurf für den nächsten C++ Standard. Zudem sind Kapitel/Absatz-Angaben in der Regel sinnvoller, weil das den Vergleich mit anderen Versionen erleichtert. Im Übrigen sieht das Beispiel völlig anders aus:

    It is possible to overload function templates so that two different function template specializations have the same type.
    [ Example:
    / / file1.c
    template < class T>
    void f(T*);
    void g(int * p) {
    f(p); / / call
    / / f<int>(int*)
    }

    / / file2.c
    template < class T>
    void f(T);
    void h(int * p) {
    f(p); / / call
    / / f<int*>(int*)
    }
    —end example ]
    2 Such specializations are distinct functions and do not violate the one definition rule (3.2).

    In diesem Beispiel sind die Überladungen in verschiedenen ÜEs definiert, eine Aussage, welche Spezialiserung bei Argumentdeduktion vorzuziehen wäre, wenn beide gleichzeitig deklariert wurden, wird hier gar nicht gemacht.



  • @camper - war total richtig was du gesagt hast... ich bin einfach zu blöd ⚠

    PS.: Ich muss mich noch bei Dir bedanken für die Erklärung zur Orthogonalität, das ich dich immer Zitiert habe war reiner Zufall, nimm's als Kompliment von mir. 👍



  • So, also laut 14.8 Function template specializations - die haben da meiner Meinung nach einen Fehler gemacht - (IMHO) müsste die nachstehende Funktion eigentlich so arbeiten:

    // file1.cpp
    template < typename T >
    void f ( T *) {};
    
    void g ( int * p )
    {
        f ( p ); // call
                 // f<int*>(int**) <-- der Typ ist int* die Funktion übernimmt Argument auf T* = int**
    }
    
    // file2.cpp
    
    template < typename T >
    void f ( T );
    
    void h ( int * p ) {
        f ( p ); // call
                 // f<int*>(int*) <-- der Typ ist int* die Funktion übernimmt ein Argument auf T = int *
    }
    

    Da jede ÜE für sich übersetzt wird muss die entsprechende Funktion (wenn nicht als extern deklariert) bereits zur Compile-Time vorhanden sein. Dh. mein Missverständnis beim zweiten Aufruf, aber was ist dann mit dem ersten Aufruf an void f ( T* ) - der funktioniert doch jetzt nach ganz anderen Regeln...

    Oder bin ich total von Rolle 😕

    Und soweit ich das überblicken kann darf in

    template < typename T >
    

    nicht anderes angegeben sein als der Typname T ohne '*' und '&'.



  • Und hier das Beispiel aus dem C++ International Standard
    Kap. 14.8.2 was mich verwirrt hat.

    template < class T > void f ( T * p )
        {
           static T s ;
           / / ...
        };
        void g ( int a , char * b )
        {
           f (& a ); // call f<int>(int*)
           f (& b ); // call f<char*>(char**)
        }
    

    Here f<int>(int*) has a static variable s of type int and f<char*>(char**) has a static variable s of type char*.

    Wenn das tatsächlich stimmen sollte das

    f<char*>(char**)
    

    übergeben wird und

    s vom Typ char*

    ist, wie könnte ich dann in der Funktion neue Stack-Objekte erstellen, etwas so:

    [cpp]template < class T > void f ( T * p )
        {
           static T s ;  // T ist vom Typ char*
           *T o1;        // das geht so nicht, nur zur Demo.
           *T* p1;       // und zurück auf Pointer natürlich so auch nicht, nur zur Demo.
           / / ...
        };
    

    Da wäre es doch in jedem Fall sinnvoller NUR den zeigerfreien Objekt-Typen zu übergeben, und der der Funktion das zu deklarieren was man Typen tatsächlich braucht.

    template < class T > void f ( T * p )
        {
           static T* s ; // T ist vom Typ  char
           T o1;         // das geht jetzt
           T* p1;        // und das natürlich auch
           / / ...
        };
    

    Hab ich das Richtig verstanden? 😕



  • DeepCopy schrieb:

    // file1.cpp
    template < typename T >
    void f ( T *) {};
    
    void g ( int * p )
    {
        f ( p ); // call
                 // f<int*>(int**) <-- der Typ ist int* die Funktion übernimmt Argument auf T* = int**
    }
    

    T ist hier dein generischer Typ. Dein Parameter ist T*, dein Argument int*. T wird versucht zu deduzieren. T wird folglich zu int ausgewertet.

    DeepCopy schrieb:

    // file2.cpp
    
    template < typename T >
    void f ( T );
    
    void h ( int * p ) {
        f ( p ); // call
                 // f<int*>(int*) <-- der Typ ist int* die Funktion übernimmt ein Argument auf T = int *
    }
    

    Andere ÜE. Kein Bezug zu file1.cpp. T ist hier dein generischer Typ. Dein Parameter ist T, dein Argument int*. T wird versucht zu deduzieren. T wird folglich zu int* ausgewertet.

    DeepCopy schrieb:

    Und soweit ich das überblicken kann darf in

    template < typename T >
    

    nicht anderes angegeben sein als der Typname T ohne '*' und '&'.

    Yes.



  • DeepCopy schrieb:

    Wenn das tatsächlich stimmen sollte das

    f<char*>(char**)
    

    übergeben wird und

    s vom Typ char*

    ist, wie könnte ich dann in der Funktion neue Stack-Objekte erstellen

    In dem Falle hast du nur noch ein T=char* und kannst auf direkten Wege keinen char's erstellen. Trait-Klassen könnten dir bei sowas helfen, siehe boost oder simple:

    template<typename T>
    struct PtrToTyp;
    
    template<>
    struct PtrToTyp<char*>
    {
        typedef char Type;
    };
    
    template <class T> 
    void f (T * p)
    {
       typename PtrToTyp<T>::Type c = 'c';
    }
    


  • @KasF:
    Es geht im um die ODR.

    Also ob es OK ist, ein Funktions-Template in einer Datei als "void f(T)" und in einer anderen als "void f(T*)" zu definieren.
    Templates haben nunmal external linkage, von daher ist die ODR hier sehr relevant.

    Ich bin mir ziemlich sicher, dass es NICHT OK ist. Sprich: eine Verletzung der ODR. Ich müsste aber auch erst nachsehen.



  • Ich dachte das wäre schon geklärt.

    hustbaer schrieb:

    Also ob es OK ist, ein Funktions-Template in einer Datei als "void f(T)" und in einer anderen als "void f(T*)" zu definieren.

    Wir haben hier einmal f**<int>(...) und f<int*>**(...), dies verletzt nicht die ODR.



  • Also ich meine den Fall, mit einmal f<int>(int*) und einmal f<int>(int).


  • Mod

    hustbaer schrieb:

    Also ich meine den Fall, mit einmal f<int>(int*) und einmal f<int>(int).

    Wieso sollte das problematisch sein? Es geht ja auch f(int) und f(int*) wenn wir es mit normalen Funktionen zu tun haben.



  • Aua, ja, danke.

    Meiner Vermutung liegt eine ziemlich peinliche Wissenslücke bezüglich Spezialisierung von Funktions-Templates zu Grunde...

    Die ich aber hier nicht weiter erklären will 🙂



  • Also wenn ich das richtig verstanden habe ist die Deduktion von generischen Typen mit dem Kürzen von Brüchen vergleichbar, wenn ich als Argument int* und Parameter T*, dann kann ich den '' bei beiden "Rauskürzen", wenn ich als Argument int und Parameter T kann ich nichts "Rauskürzen" also bleibt int* übrig, also kurz: Immer der "meist spezialisierte Typ" nach dem "Kürzen" ist anzuwenden.

    Die Deduktion scheint immer angewandt zu werden, egal ob das mit den Funktionssignaturen nachher kompatibel ist oder nicht, d.h. wenn ich ein int übergebe und mein Parameter will T*, wird sich der Compiler nach der Deduktion beschweren.

    So weit, so gut. Aber: was ist wenn als Argument int& und Parameter T* gefordert sind, oder auch anderes herum, wenn int* als Argument und Parameter T& gefordert werden? Hier kann ich nicht "Kürzen" da einmal eine Referenz und einmal ein Zeiger vorhanden ist, was ist das Ergebnis der Deduktion? Bleibt "int*" oder "int&", darf hier die Spezialisierung angewendet werden?



  • DeepCopy schrieb:

    Die Deduktion scheint immer angewandt zu werden, egal ob das mit den Funktionssignaturen nachher kompatibel ist oder nicht, d.h. wenn ich ein int übergebe und mein Parameter will T*, wird sich der Compiler nach der Deduktion beschweren.

    Er wird sich nur dann beschweren, wenn es sonst keine andere Funktion gibt, die aufgerufen werden kann. Es können auch nicht alle Typen deduziert werden. Beispiel:

    template<typename T> struct identity {typedef T type;};
    
    template<typename T> void foo(typename identity<T>::type x);
    
    int main() {
      foo(4);       // Fehler! Compiler kann T nicht deduzieren, ignoriert
                    // deswegen das Funktions-Template und findes keine
                    // andere Funktion, die aufgerufen werden kann.
    
      foo<int>(23); // OK!
    }
    

    "identity<T>::type" ist für T kein "deduzierbarer Kontext". Wichtig ist auch, dass wenn der Compiler versucht Template-Parameter zu deduzieren, und ein zu deduzierender Template-Parameter in mehreren "deduzierbaren Kontexten" auftaucht, die Dedukition fehlschlägt, falls er auf unterschiedliche Ergebnisse kommt. Beispiel

    template<typename T> void bar(T x, T y);
    template<typename T> void baz(T x, typename identity<T>::type y);
    
    int main() {
      bar(3,24L); // Fehler! T nicht deduzierbar, weil einmal T=int
                  // und dann T=long rauskommt
    
      baz(3,24L); // OK, T=int, 24L wird zu einem int implizit konvertiert.
    }
    

    DeepCopy schrieb:

    So weit, so gut. Aber: was ist wenn als Argument int& und Parameter T* gefordert sind, oder auch anderes herum, wenn int* als Argument und Parameter T& gefordert werden? Hier kann ich nicht "Kürzen" da einmal eine Referenz und einmal ein Zeiger vorhanden ist, was ist das Ergebnis der Deduktion? Bleibt "int*" oder "int&", darf hier die Spezialisierung angewendet werden?

    Da gibt es so einen Begriff: SFINAE = substitution failure is not an error. Wenn das Deduzieren nicht gelingt, wird das Template einfach ignoriert, ohne einen Fehler zu generieren. Wenn allerdings keine aufrufbare Funktion bzw Spezialisierung übrig bleibt und das "overload set" leer ist, gibt es natürlich einen Fehler beim Kompilieren.

    Top-Level Referenzen werden mehr oder weniger bei der Deduktion ignoriert, bis auf dass array-to-pointer decay bzw function-to-pointer decay nicht sofort angewendet wird:

    template<typename T> void dings(T);
    template<typename T> void blah (T*);
    template<typename T> void bums (T&);
    int main()
    {
      int array[5];
      dings(array); // T = int*   --> void dings(int*);
      blah (array); // T = int    --> void blah (int*);
      bums (array); // T = int[5] --> void bums (int(&)[5]);
    }
    

    "X-to-pointer decays", "implicit cv qualification conversion" und Konvertierung von Derived auf Base sind zulässig bei der Deduktion, alle anderen impliziten Konvertieren nicht.

    Edit: Um die Beispiele zu vervollständigen:

    template<typename T> struct A {};
    
    struct B : A<int> {};
    
    template<typename T>
    void foo(A<T> const& x) {}
    
    int main() {
        B b;
        foo(b); // T = int
    }
    

    "A<T> const&" ist für T ein deduzierbarer Kontext und die Deduktion gelingt auch dann, wenn foo mit einem abgeleiteten Objekt aufgerufen wird. Hier findet sowohl eine Derived-to-Base Konvertierung als auch eine "cv qualification conversion" statt.

    Wer sich für all die Details interessiert, sollte sich C++ Templates | ISBN: 9780201734843 mal anschauen. Ich kann das Buch nur empfehlen. Mir sind so einige Lichter beim Lesen aufgegangen.

    Gruß,
    SP


Anmelden zum Antworten