std::min std::max by value oder by reference ?



  • Hallo,
    die std::max() sieht ja ca. so aus:

    T const & max(T const & lhs, T const & rhs)
    {
        return lhs < rhs ? rhs : lhs;
    }
    

    Warum nimmt man die auch für Zahlen, und nicht pass-by-value (um deref zu sparen)?
    Warum macht max(1.23, 4.56) kein Probleme?
    Müsste ich nciht eigentlich die Parameter und/oder Rückgabe by-value machen??


  • Mod

    madmax0 schrieb:

    Warum nimmt man die auch für Zahlen, und nicht pass-by-value (um deref zu sparen)?

    Ist doch sowieso ein Template, liegt also direkt als Quellcode vor, und kann daher mit Leichtigkeit optimiert werden.

    Warum macht max(1.23, 4.56) kein Probleme?

    Welches Problem erwartest du? Machst du dir Sorgen um die Lebenszeit?

    Müsste ich nciht eigentlich die Parameter und/oder Rückgabe by-value machen??

    Offensichtlich nicht. Warum denkst du, müsste das so sein?



  • (aus gcc STL - Einrückung angepasst auf 4 Leerzeichen. Leerzeichen und Tabs waren vermischt.)

    template<typename _Tp>
    _GLIBCXX14_CONSTEXPR
    inline const _Tp&
    max(const _Tp& __a, const _Tp& __b)
    {
        // concept requirements
        __glibcxx_function_requires(_LessThanComparableConcept<_Tp>)
        //return  __a < __b ? __b : __a;
        if (__a < __b)
            return __b;
        return __a;
    }
    

    Ich will zu SeppJ nichts hinzufügen, das war explizit an dich gerichtet.
    Aber ich dachte mir nur: "wo ist constexpr?" und hab kurz nachgeschaut.
    Interessant ist auch, dass hier die trinärer Operator Variante auskommentiert ist und if benutzt wurde. Kann der compiler wohl besser mit arbeiten.



  • madmax0 schrieb:

    Warum nimmt man die auch für Zahlen, und nicht pass-by-value (um deref zu sparen)?

    Das ist halt im ISO Standard so seit 1998, eine einfache generische Formulierung, die Kopien in allen Fällen vermeidet. Wenn du nur in bestimmten Fällen die Referenzen weglassen willst (kleine POD Typen), wann wär das eben viel umständlicher. Du müsstest das Template auf eine Art und Weise überladen, ohne dass es zu Mehrdeutigkeiten führt. Das könnte man heutzutage per SFINAE machen:

    template<class T>
    typedef std::enable_if< std::is_trivial<T>::value && sizeof(T)<=sizeof(long),
    T >::type min(T const x, T const y)
    { return y < x ? y : x; }
    
    template<class T>
    typedef std::enable_if< !(std::is_trivial<T>::value && sizeof(T)<=sizeof(long)),
    T const& >::type min(T const& x, T const& y)
    { return y < x ? y : x; }
    

    Ich denke aber nicht, dass das den Aufwand wert ist.

    madmax0 schrieb:

    Warum macht max(1.23, 4.56) kein Probleme?
    Müsste ich nciht eigentlich die Parameter und/oder Rückgabe by-value machen??

    Kommt drauf an, wie du es einsetzt.

    double v = std::min(1.23, 4.56); // kein Problem
    

    Die temporären double-Objekte verschwinden quasi erst beim Semikolon, also nachdem alles fertig ausgewertet ist.

    const double& ref = std::min(1.23, 4.56); // baumelnde Referenz
    

    Hier wurde ref mit etwas initialisiert, was sich auf ein temporäres double-Objekt bezieht, was dann aber verschwindet. ref ist damit ungültig.

    Lustigerweise ist das hier voll OK:

    const double& ref = 1.23;
    

    Hier greift nämlich eine Sonderregel, die die Lebenszeit des temporären Objekts verlängert. Dazu muss der Compiler aber ohne großen Analyse-Aufwand schon feststellen können, worauf sich ref immer bezieht und das geht eigentlich nur bei "pure rvalues".

    Das hier knallt wieder:

    const double& ref = std::move(1.23); // ungueltige Referenz
    


  • krümelkacker schrieb:

    const double& ref = std::min(1.23, 4.56); // baumelnde Referenz
    

    Interessanter Fall über den ich mir noch wenig Gedanken gemacht habe. Ich könnte mir vorstellen, dass solche Fälle durchaus in weniger offensichtlicher Form auftreten können z.B. bei der Benutzung innerhalb eines Templates wo beim Programmieren noch gar nicht feststeht, was für einen Typ die Parameter genau haben werden. Ist ja kein völlig exotischer Anwendungsfall, dass man sich das größte Element für später merken möchte, ohne eine Kopie zu erzeugen. Könnte mir vorstellen, dass dies zu sehr schwer zu diagnostizierenden Fehlern führen kann oder irre ich mich da?


  • Mod

    TNA schrieb:

    krümelkacker schrieb:

    const double& ref = std::min(1.23, 4.56); // baumelnde Referenz
    

    Interessanter Fall über den ich mir noch wenig Gedanken gemacht habe. Ich könnte mir vorstellen, dass solche Fälle durchaus in weniger offensichtlicher Form auftreten können z.B. bei der Benutzung innerhalb eines Templates wo beim Programmieren noch gar nicht feststeht, was für einen Typ die Parameter genau haben werden. Ist ja kein völlig exotischer Anwendungsfall, dass man sich das größte Element für später merken möchte, ohne eine Kopie zu erzeugen. Könnte mir vorstellen, dass dies zu sehr schwer zu diagnostizierenden Fehlern führen kann oder irre ich mich da?

    Hast du jemals eine Referenz auf das Ergebnis einer Rechenoperation gespeichert?



  • @ SeppJ:

    Warum denkst du, müsste das so sein?

    (um deref zu sparen)

    Ist doch sowieso ein Template

    Davon habe ich nichts geschrieben. Mir gehts auch eher um Sprachfeatures und nicht Compilerfeatures.
    Das sind ja mehr Fragen, als ich habe....

    @ 5cript:

    Interessant ist auch, dass hier die trinärer Operator Variante auskommentiert ist und if benutzt wurde. Kann der compiler wohl besser mit arbeiten.

    Hat, soweit ich weiß, was mit RVO zutun.

    @ krümelkacker: Danke für die Antwort. Mit dem Kram komme ich öfters durcheinander. Ich nutze aber auch selten Referenzen als reines Alias, wenn ich einfach das Original nutzen kann (ist evtl ein Henne-Ei-Problem).
    Was ist mit:

    double &d = double(void); //Pseudo: Soll eine Funktions-Signatur sein
    

    ?
    Warum ist die Referenzierung von etwas ohne Namen auf dem Stack überhaupt erlaubt....??? Gibts dagegen nicht ne Compiler-Warnung?

    Aber jetzt zum eigentlichen Punkt: Mit deiner SFINAE-Struktur bringst du mich echt durcheinander.. Sah bei mir immer anders aus. Eher Test in der Spezialisierung etc.
    Wozu das typedef? Und dann noch die Überprüfung des return-Typs??!
    Kannst du mir evtl. das ganze kurz als Pseudocode aufmalen?

    Ich denke aber nicht, dass das den Aufwand wert ist.

    Können die Compiler-Leute doch jetzt von dir copy-&-paste-n..



  • SeppJ schrieb:

    TNA schrieb:

    krümelkacker schrieb:

    const double& ref = std::min(1.23, 4.56); // baumelnde Referenz
    

    Interessanter Fall [...]

    Hast du jemals eine Referenz auf das Ergebnis einer Rechenoperation gespeichert?

    Die for-range Schleife macht übrigens etwas ähnliches:

    for (char c : mystring) {
        cout << c;
    }
    

    ist quasi nur eine Abkürzung für

    {
        using std::begin;
        using std::end;
        auto&& __range = mystring; // <-- hier will man unnötige Kopien vermeiden
        for (auto __iter = begin(__range),
                  __end = end(__range); __iter != __end; ++__iter)
        {
            char c = *__iter;
            cout << c;
        }
    }
    

    Das heißt, folgendes wäre eine echt blöde Idee:

    template<class T>
    T const& mist(T const& x) { return x; }
    
    ...
    
    for (char c : mist(mystring + "\n")) {
        cout << c;
    }
    

    (das temporäre string -Objekt wird vor dem Iterieren schon wieder zerstört)



  • madmax0 schrieb:

    Was ist mit:

    double &d = double(void); //Pseudo: Soll eine Funktions-Signatur sein
    

    ?
    Warum ist die Referenzierung von etwas ohne Namen auf dem Stack überhaupt erlaubt....??? Gibts dagegen nicht ne Compiler-Warnung?

    Das Beispiel verstehe ich nicht ganz. Meintest du so etwas wie das hier?

    doube funktion()
    {
        return 1.23;
    }
    
    int main() {
        const double& x = funktion();
    }
    

    ? Wenn ja: Das ist voll OK so. Es bringt dir aber im Vergleich zu

    int main() {
        const double x = funktion();
    }
    

    rein gar nichts.

    Das hier:

    int main() {
        double & x = funktion();
    }
    

    würde nicht kompilieren, weil man eine nicht-const lvalue-Referenz so nicht (also mit einem Rvalueausdruck) initialisieren kann.

    madmax0 schrieb:

    Aber jetzt zum eigentlichen Punkt: Mit deiner SFINAE-Struktur bringst du mich echt durcheinander.. Sah bei mir immer anders aus. Eher Test in der Spezialisierung etc.
    Wozu das typedef? Und dann noch die Überprüfung des return-Typs??!
    Kannst du mir evtl. das ganze kurz als Pseudocode aufmalen?

    Sorry, da habe ich mich vertippt. Es sollte typename statt typedef sein.

    madmax0 schrieb:

    Ich denke aber nicht, dass das den Aufwand wert ist.

    Können die Compiler-Leute doch jetzt von dir copy-&-paste-n..

    wozu?



  • SeppJ schrieb:

    Hast du jemals eine Referenz auf das Ergebnis einer Rechenoperation gespeichert?

    Die Anwendung von std::min geht weit über Rechenoperationen mit fundamentalen Datentypen hinaus, wie ich versucht habe zu erläutern. Letztendlich kann das für alle Klassen verwenden, die den Kleiner-Operator überladen. Wenn man das generisch verwenden möchte, muss man ja auch damit rechnen, das ein Objekt eventuell nur aufwendig oder gar nicht kopierbar ist.



  • krümelkacker schrieb:

    ? Wenn ja: Das ist voll OK so.

    Wegen Lebenzeitverlängerung?!
    Angenommen das stimmt, warum wird nicht hier auch irgendwo die Lebenszeit verlängert?

    const double & funktion()
    {
        return 1.23;
    }
    
    int main() {
        const double& x = funktion();
    }
    

    krümelkacker schrieb:

    Es bringt dir aber im Vergleich zu
    rein gar nichts.

    Dann frage ich mich erst recht, warum soetwas überhapt erlaubt ist. Je nach Funktions-Rückgabe-Art kann es ja Probleme bereiten...

    double & x = double(void)/*Fkt-Signatur*/; // nicht erlaubt
    const double & x = double(void)/*Fkt-Signatur*/; // erlaubt
    

    Warum ist das Eine denn erlaubt und das Andere nicht? Kannst du mir erklären, was da unter der Haube passiert?
    Ich stelle mir das so vor, dass die Rückgabe ja irgendwo gespeichert sein muss. Und dann kann es dem Compiler doch egal sein, ob man sie verändern kann oder nicht..

    krümelkacker schrieb:

    wozu?

    Wäre dein Bsp. mit der for-Schleife nicht so ein Fall??? Ohne Refs würde der doch keine Probleme machen..



  • madmax0 schrieb:

    Warum ist das Eine denn erlaubt und das Andere nicht? Kannst du mir erklären, was da unter der Haube passiert?
    Ich stelle mir das so vor, dass die Rückgabe ja irgendwo gespeichert sein muss. Und dann kann es dem Compiler doch egal sein, ob man sie verändern kann oder nicht..

    Weil es nunmal so festgelegt wurde. const Referenzen können auch rvalues (also temporäre Objekte) binden und normale Referenzen eben nicht. Seit C++11 gibts extra rvalue Referenzen, damit verlängerst du auch die Lebenszeit und kannst den Wert danach noch verändern. Das verlängern der Lebenszeit funktioniert aber nur beim Zuweisen von temporären Werten an eine Referenz und bei Funktionsparametern. Nicht bei Rückgabewerten.



  • sebi707 schrieb:

    Weil es nunmal so festgelegt wurde.

    Das sage ich mir auch oft, bis sich dann herausstellt, dass es dafür einen tieferen Grund gibt.
    Um sowas rauszufinden muss man leider viel Glück haben und sich durch viele SO-Beiträge arbeiten.

    sebi707 schrieb:

    Seit C++11 gibts extra rvalue Referenzen

    Klingt für mich schon nach einem Indiz für igendwelche Inkompabilitäten, die dann entstünden. Sonst hätte man die normalen Refs ja auch einfach erweitern können, statt neue einzuführen. ..Zumindest für den Fall. Die rvalue-Refs haben bestimmt noch andere Vorteile.

    ..Sepp braucht wohl etwas Beschäftigung. Was so angenehm ruhig hier, als er noch in dem anderen Board zutun hatte.


Log in to reply