Wozu decltype, wenn man auto hat?



  • Nexus schrieb:

    krümelkacker schrieb:

    und dann ist auto ja auch nur ein Typ-Platzhalter, der entsprechend dekoriert werden kann:

    vector<auto> blah = myfunction();
    

    Ah, das kannte ich ebenfalls nicht. Interessant! 💡

    Naja, gcc kennt das auch nicht.
    Ist es wirklich erlaubt?

    Außerdem habe ich Angst vor

    auto<auto> blah = myfunction();
    

    :xmas2:



  • @Nexus:
    Ein Lambdaausdruck darf nicht innerhalb sizeof und decltype auftauchen. Und da {2,3,5,7} kein richtiger Ausdruck ist, klappt hier decltype auch nicht. Aber für auto gibt es eine Sonderregel:

    auto list = {2,3,5,7,11};
    // decltype(list) -> std::initializer_list<int>
    

    Damit funktioniert dann auch das hier:

    for (int x : {2,3,5,7,11}) {
      cout << x << endl;
    }
    

    weil hier hinter der Kulisse so auto verwendet wird:

    {
      auto && __range = {2,3,5,7,11};
      for (auto it = begin(__range), ee = end(__range);
           it != ee; ++it)
      {
        int x = *it;
        cout << x << endl;
      }
    }
    


  • Danke für die Erklärung.

    Warum die Regel, dass man kein decltype auf Lambdas und Initializer-Lists anwenden kann? Indirekt kommt man durch Variablen sowieso an den Typen.

    Und wieso ist {2,3,5,7} kein richtiger Ausdruck? Weil es unter Anderem als Argumentliste für den Konstruktor oder als Aggregat-Initialisierungsliste verwendet werden kann?



  • {
      auto && __range = {2,3,5,7,11};
      int x;
      for (auto it = begin(__range);it != end(__range); ++it)
      {
        x = *it;
        cout << *it << '\n';
      }
    }
    

    1. Ist es so schlimm, wenn end() immer wieder aufgerufen werden muss?
    2. Wird x nicht globaler deklariert?

    Edit: ach ja, und- was genau ist end()? Kann dazu nichts finden... findet es das Ende eines C-Arrays, oder wa?



  • Hacker schrieb:

    Edit: ach ja, und- was genau ist end()? Kann dazu nichts finden... findet es das Ende eines C-Arrays, oder wa?

    Ja. Und bei normalen Containern ruft es cont.end() auf. Ganz automatisch. Wohl mit SFINAE implelentiert.



  • Edit: ach ja, und- was genau ist end()? Kann dazu nichts finden... findet es das Ende eines C-Arrays, oder wa?

    Schau mal hier: http://en.cppreference.com/w/cpp/iterator/end

    Das sollte dir weiterhelfen 🙂


  • Mod

    Nexus schrieb:

    Danke für die Erklärung.

    Warum die Regel, dass man kein decltype auf Lambdas und Initializer-Lists anwenden kann? Indirekt kommt man durch Variablen sowieso an den Typen.

    Und wieso ist {2,3,5,7} kein richtiger Ausdruck? Weil es unter Anderem als Argumentliste für den Konstruktor oder als Aggregat-Initialisierungsliste verwendet werden kann?

    Unter anderem das. Außerdem kann man initialiser_list<A> nicht in initialiser_list<B> konvertieren. Andererseits ist

    struct foo { foo(std::initialiser_list<int>); };
    
    foo x = { 'a', 'b' }; // ok
    

    kein Problem, aus dem Kontext der Initialisierung heraus ergibt sich, dass ein std::initialiser_list<int> benötigt wird.

    Hacker schrieb:

    {
      auto && __range = {2,3,5,7,11};
      int x;
      for (auto it = begin(__range);it != end(__range); ++it)
      {
        x = *it;
        cout << *it << '\n';
      }
    }
    

    1. Ist es so schlimm, wenn end() immer wieder aufgerufen werden muss?
    2. Wird x nicht globaler deklariert?

    Edit: ach ja, und- was genau ist end()? Kann dazu nichts finden... findet es das Ende eines C-Arrays, oder wa?

    krümelkackers Code sieht wie aus dem Standard kopiert aus. Da __range im allgemeinen Fall des range-based for-loops auch ein normaler Container sein kann, gibt es semantische Unterschiede zwischen beiden Varianten. Da die Spezifikation ist wie sie ist, wird damit ausgedrückt, dass der end-Iterator in einem solchen loop gültig bleiben muss.



  • volkard schrieb:

    Wohl mit SFINAE implelentiert.

    http://en.wikipedia.org/wiki/Substitution_failure_is_not_an_error, ganz versteh ich das noch nicht. Also, man kann beliebige Template-Parameter übergeben, und es kommt zu keinem Fehler?

    struct Test {
        typedef int foo;
    };
    
    template <typename T> 
    void f(typename T::foo) {} // Definition #1
    
    template <typename T> 
    void f(T) {}                // Definition #2
    
    int main() {
        f<Test>(10); // Call #1.
        f<int>(10);  // Call #2. Without error (even though there is no int::foo) thanks to SFINAE.
    }
    

    Das verwirrt mich aufs extremste...T::foo gibt es ja sowieso nur bei Test, oder etwa nicht? also müsste bei Call#1 Def#1 und bei Call#2 entsprechen Def#2 aufgerufen werden? Logischerweise?

    :xmas1:



  • Nexus schrieb:

    Warum die Regel, dass man kein decltype auf Lambdas und Initializer-Lists anwenden kann?

    Jedes Lambda hat seinen eigenen Typ, egal, ob es völlig mit einem anderen Lambda übereinstimmt oder nicht. Sonst müsste der Compiler in der Funtkion alle Lambdas auf Äquivalenz untersuchen und ggf. zusammenfügen. Und weil in dem AUTO()-Makro eben zwei Lambdas vorkommen, die zwar äquivalent sind, aber vom Compiler nicht als solche erkannt werden (müssen), gibt es keinen Ausdruck, den du rechts hinschreiben könntest, um den Typ des decltypes links zu bekommen.

    (Alle Angaben ohne Gewähr)



  • @Hacker: Richtig. SFINAE hat viel mit Template-Spezialisierung und Fallunterscheidung zu tun. Dein Programm, das du richtig analysiert hast, ist noch die einfache Variante. Kompliziertere Varianten benutzen Funktionen/Klassen, die für verschiedene Typen überladen/spezialisiert sind und verschiedene Rückgabetypen mit verschiedenen Größen haben, werten dann die Größe des Rückgabetyps für die Überladung, die der Compiler für den gegebenen Typ auswählt, aus und benutzen je nach Größe eine unterschiedliche Klassenspezialisierung.

    Schau dir auch mal Type Traits wie z.B. std::is_base_of, std::is_same, std::is_integral o.ä. an.



  • Hacker schrieb:

    Das verwirrt mich aufs extremste...T::foo gibt es ja sowieso nur bei Test, oder etwa nicht? also müsste bei Call#1 Def#1 und bei Call#2 entsprechen Def#2 aufgerufen werden? Logischerweise?

    Ganz logisch ist das nicht. Wenn der Standard SFINAE nicht definieren wuerde, wuerde der compiler beim 2. fall versuchen int::foo zu erzeugen und einen Fehler schmeissen. Durch SFNIAE wird dem compiler aber mitgeteilt: wenn es int::foo nicht gibt, ignoriere das erstmal und such nach einer anderen Version der Funktion, die funktioniert. Das ist der Kern der Regel.

    In diesem Fall wird SFINAE angesprochen, weil man damit gezielt funktionen ausschalten kann, wenn der Templateparameter ebstimmte Eigenschaften hat.

    Zum Beispiel kannst du sowas machen:

    template<bool T>
    struct TriggerSFINAE{};
    template<>
    struct TriggerSFINAE<true>{typedef int* type;};
    
    template<class T>
    struct IsArithmetic{
        static const bool value = false;
    };
    template<>
    struct IsArithmetic<double>{
        static const bool value = true;
    };
    ...
    
    //und dann sowas wie:
    
    //die funktion soll nur auf arithmetischen typen funktionieren:
    template<class T>
    void foo(T t,typename TriggerSFINAE<IsArithmetic<T>::value>::type t = nullptr){
        std::cout<<"ist arithmetisch";
    }
    //hier ne implementation die sonst aufgerufen wird:
    void foo(T t,typename TriggerSFINAE<!IsArithmetic<T>::value>::type t = nullptr){
        std::cout<<"ist nicht arithmetisch";
    }
    

    Das ist besonders sinnvoll, wenn der Funktionsaufruf sonst mehrdeutig waer, also von der Parameterliste beide Versionen passen wuerden.



  • otze schrieb:

    Hacker schrieb:

    Das verwirrt mich aufs extremste...T::foo gibt es ja sowieso nur bei Test, oder etwa nicht? also müsste bei Call#1 Def#1 und bei Call#2 entsprechen Def#2 aufgerufen werden? Logischerweise?

    Ganz logisch ist das nicht. Wenn der Standard SFINAE nicht definieren wuerde, wuerde der compiler beim 2. fall versuchen int::foo zu erzeugen und einen Fehler schmeissen. Durch SFNIAE wird dem compiler aber mitgeteilt: wenn es int::foo nicht gibt, ignoriere das erstmal und such nach einer anderen Version der Funktion, die funktioniert. Das ist der Kern der Regel.

    Ah, jetzt verstehe ich etwas. Das Problem ist, ich dachte dass eben das selbstverständlich ist, weshalb ich zuerst dachte, SFINAE sei irgendeine Erweiterung 😃

    :xmas1:


  • Administrator

    volkard schrieb:

    Nexus schrieb:

    krümelkacker schrieb:

    und dann ist auto ja auch nur ein Typ-Platzhalter, der entsprechend dekoriert werden kann:

    vector<auto> blah = myfunction();
    

    Ah, das kannte ich ebenfalls nicht. Interessant! 💡

    Naja, gcc kennt das auch nicht.
    Ist es wirklich erlaubt?

    Das würde mich auch wunder nehmen. Konnte auf die schnelle nichts im Standard finden.

    Dafür habe ich etwas anderes gefunden, was ich noch nicht kannte:

    auto x = new auto('c');
    

    Grüssli



  • Der - für mich - naheliegendste Grund warum decltype gebraucht wird wurde noch nicht genannt (oder ich hab's übersehen):
    Mit auto kann man keine Return-Typen zusammenbauen.
    Mit decltype dagegen schon.

    auto Foo() -> decltype(A() + B());
    

    Bzw. allgemein überall wo man den Typ eines Ausdrucks bestimmen will ohne gleich eine Variable zu definieren.


Anmelden zum Antworten