Warum sind rvalues von nicht-builtin-Typen nicht automatisch const?



  • wxSkip schrieb:

    [betreff]

    Das Lvalue/Rvalue Konzept ist nahezu orthogonal zu Constness. "Nahezu" deswegen, weil eine lvalue-referenz-auf-const mit einem rvalue initialisiert werden kann. Das ist einfach eine praktische Ausnahmeregel bzgl Referenzinitialisierung.

    Ich glaube auch, dass die Wortwahl der Frage etwas ungeschickt war. Es geht hier doch nicht im Constness, sondern darum, dass du Methoden auf temporären Objekten aufrufen kannst. Temporäre Objekte sind glücklicherweise nicht unbedingt const. Das wär ja auch echt schade, wenn dem so wäre. (siehe "move semantics").

    wxSkip schrieb:

    Die Tatsache, dass auf rvalues von nicht eingebauten Typen auch nicht-const-Memberfunktionen aufgerufen werden können, hat gerade mein Weltbild ein bisschen beschädigt.

    Ist einfach so. Du kannst aber in C++2011 mit sogenannten "Referenz-Qualifizierern" einschränken, welche Methoden wann aufgerufen werden können:

    struct foo {              // aufrufbar auf
      // ohne Ref-Qualifier:
      void a() const;         // C+NC L+R
      void b();               //   NC L+R
    
      // mit Ref-Qualifier:
      void c() const &;       // C+NC L[b]+R[/b] (+R wegen des "praktischen Sonderfalls")
      void d()       &;       //   NC L
      void e() const &&;      // C+NC   R
      void f()       &&;      //   NC   R
    };
                              // C = const, NC = non-const
                              // L = Lvalue, R = Rvalue
    

    Wenn man Funktionen überladen will und es stimmen Parameterzahl und -typen überein, so müssen alle Overloads entweder ohne ref-qualifier auskommen, oder sie haben alle einen Ref-Qualifier.

    Auch möglich:

    struct bar {
      string member;
      bar(bar const&) = default;
      bar(bar     &&) = default;
      bar& operator=(bar const&) [b]&[/b] = default;
      bar& operator=(bar     &&) [b]&[/b] = default;
    }; //                       ^^^
    

    wxSkip schrieb:

    [...] (Mit rvalues meine ich jetzt temporäre Objekte (also Rückgabewerte von Funktionen/Operatoren), keine als const deklarierten Variablen).

    Klar. Die Namen der per const deklarierten Variablen sind ja auch keine rvalues.

    wxSkip schrieb:

    @knivil:
    1. Weil rvalues von builtin-Typen sich const verhalten. D.h.:

    class Test{};
    Test foo1(){return Test();}
    int foo2(){return 5;}
    
    int main()
    {
        foo1() = foo1(); //geht
        foo2() = foo2(); //Compilerfehler
    }
    

    Rvalues von eingebauten Typen beziehen sich gar nicht auf irgendwelche Objekte. Von daher kann man da auch nirgends etwas zuweisen. Rvalues von Klassentypen hingegen beziehen sich auf im Speicher stehende Objekte. Da macht eine Zuweisung schon etwas mehr Sinn. Wenn man das nicht möchte, kann man das jetzt mit einem & beim Zuweisungsoperator ausschalten.

    wxSkip schrieb:

    2. Weil noch andere Leichtsinnsfehler damit möglich sind:

    std::string foo1();
    std::string foo2();
    
    int main()
    {
        if(foo1() = foo2()) std::cout << "FAIL\n";  //kein Compilerfehler
    }
    

    Dann wirst Du das mit den Ref-Qualifizierern mögen. 🙂

    wxSkip schrieb:

    Ein Grund dafür ist natürlich, rvalues modifizieren und dann wieder an eine andere Funktion übergeben zu können.

    ... "Move Semantics" ... der "vector_typ().swap(anderer_vector)"-Trick ...

    std::auto_ptr kann man auch nicht ohne dieses Feature (dass man nonconst-Methoden auf rvalues aufrufen kann) implementieren. Nicht, dass, std::auto_ptr jetzt was tolles wäre....


  • Mod

    wxSkip schrieb:

    @knivil:
    1. Weil rvalues von builtin-Typen sich const verhalten. D.h.:
    ...
    2. Weil noch andere Leichtsinnsfehler damit möglich sind:

    Damit zäumst du das Pferd von hinten auf.
    Diese Art von Verbesseungsvorschlag deutet darauf hin, dass du das Wesen von l- und rvalues noch nicht verstanden hast.
    Natürlich könnte man die Sprache so modifizieren, dass Klassen-rvalues grundsätzlich const-qualifiziert sind. Das wäre aber immer noch etwas ganz anderes als bei skalaren Typen - und würde bestimmt eine Menge Anfänger dazu bringen, hier zu posten weil Code plötzlich nicht mehr funktioniert. 😉



  • wxSkip schrieb:

    1. Weil rvalues von builtin-Typen sich const verhalten. D.h.:

    Die 5 in deinem Beispiel ist kein L-Value, da kein Speicher mit ihr assoziiert ist.

    2. Weil noch andere Leichtsinnsfehler damit möglich sind:

    Zuweisungen dieser Art innerhalb von if sind Anfaengerfehler. Die macht man nach einer gewissen Zeit nicht mehr. Darueber hinaus gibt es Codechecker, die soche Fehler leicht finden. Diesen Grund lasse ich nicht gelten.

    Ein Grund dafür ist natürlich, rvalues modifizieren und dann wieder an eine andere Funktion übergeben zu können.

    Ja, wahrscheinlich koennte ich sonst nicht D = (A*B - C).transpose() schreiben, wenn A,B,C und D Matrizen sind.



  • wxSkip schrieb:

    2. Weil noch andere Leichtsinnsfehler damit möglich sind:

    std::string foo1();
    std::string foo2();
    
    int main()
    {
        if(foo1() = foo2()) std::cout << "FAIL\n";  //kein Compilerfehler
    }
    

    Ein

    cout<<(foo+bar).invert();
    

    waere aber uU wuenschenswert?

    verbieten ist immer doof. erlauben ist immer toll.



  • knivil schrieb:

    wxSkip schrieb:

    1. Weil rvalues von builtin-Typen sich const verhalten. D.h.:

    Die 5 in deinem Beispiel ist kein L-Value, da kein Speicher mit ihr assoziiert ist.

    Das gilt nicht. Ich hätte genausogut return int(5); schreiben können, was sozusagen ein (inline-)Konstruktoraufruf und damit eine Speicherassoziierung ist, aber trotzdem nicht funktioniert.

    @camper: Wenn ich lvalues und rvalues noch nicht verstanden habe (in der Tat verstehe ich die rvalue-Referenzen und das move-Zeug auch noch nicht so gut), hast du dann auch einen Link dazu?

    Shade Of Mine schrieb:

    verbieten ist immer doof. erlauben ist immer toll.

    Das hätte ich jetzt nicht von einem erfahrenen Programmierer erwartet. ⚠

    Wenn ihr wissen wollt, wie ich draufgekommen bin: http://gotw.ca/gotw/006.htm (und auch bei Effective C++): möglichst alle nicht-builtin-Rückgabewerte const machen. (geht außerdem bei einem Konstruktor ja nicht)



  • Das hätte ich jetzt nicht von einem erfahrenen Programmierer erwartet.

    Sind wir jetzt im Kindergarten?



  • knivil schrieb:

    Das hätte ich jetzt nicht von einem erfahrenen Programmierer erwartet.

    Sind wir jetzt im Kindergarten?

    😃 Nein.
    Aber grundsätzlich Compiler-Restriktionen abzulehnen finde ich ... naja.


  • Mod

    wxSkip schrieb:

    Ich hätte genausogut return int(5); schreiben können, was sozusagen ein (inline-)Konstruktoraufruf und damit eine Speicherassoziierung ist, aber trotzdem nicht funktioniert.

    Nur Klassen verfügen über Konstruktoren. int(5) ist kein Konstruktoraufruf und erzeugt auch kein Objekt, nicht mal ein Pseudo-objekte.
    Skalare Typen haben eine wichtige Eigenschaft: Die eingebauten Operatoren können unmittelbar mit Werten (d.h. Objektrepräsentationen a.k.a. Bitmuster) dieser Typen umgehen. Im Ausdruck

    5+5
    

    ist kein Objekt im Spiel. Die Klassifizierung in l- und r-values wurde ja gerade deshalb eingeführt, um diesen Unterschied zwischen Objekten und Werten formal zu erklären

    42 = 42; // (1)
    const int foo = 0;
    foo = 42; // (2)
    

    Beide Zuweisungen sind fehlerhaft, und in beiden Fällen könnte man sagen, dass die linke Seite "konstant" ist. Die Natur dieser Konstanz ist aber völlig verschieden.

    Für Klassen und Arrays gilt das nicht: es ist in C++ unmöglich, auf den Wert eine Klasse auf eine Wesie zu verweisen, wie es (1) tut. Der Zustand von Klassen existiert niemals losgelöst von einem konkreten Objekt (Array-rvalues sind sowieso exostische Kreaturen, die nur über Klassen-rvalues erzeugt werden können). Du müsstest also das Objektmodel von C++ erheblich verändern, um hier eine Angleichung an skalare Typen zu erreichen.
    Ist dein Konstanz dagegen im Sinne von const-qualifiziert zu verstehen, dann ist das auch nur willkürlich: skalare rvalues sind niemals cv-qualifiziert, eben weil es keine Objekte sind.

    wxSkip schrieb:

    Wenn ihr wissen wollt, wie ich draufgekommen bin: http://gotw.ca/gotw/006.htm (und auch bei Effective C++): möglichst alle nicht-builtin-Rückgabewerte const machen. (geht außerdem bei einem Konstruktor ja nicht)

    Ja. Hier schiesst Sutter übers Ziel hinaus.



  • Shade Of Mine schrieb:

    verbieten ist immer doof. erlauben ist immer toll.

    Ja, diese ganzen Verbote durch Const-Correctness sind schlimm.



  • wxSkip schrieb:

    Wenn ihr wissen wollt, wie ich draufgekommen bin: http://gotw.ca/gotw/006.htm (und auch bei Effective C++): möglichst alle nicht-builtin-Rückgabewerte const machen.

    Diese Regel ist heute nicht mehr zu gebrauchen und war früher mindestens streitbar. Das ist auch direkt das erste Wort des vierten Punktes auf der Seite, "arguable".

    wxSkip schrieb:

    @camper: Wenn ich lvalues und rvalues noch nicht verstanden habe (in der Tat verstehe ich die rvalue-Referenzen und das move-Zeug auch noch nicht so gut), hast du dann auch einen Link dazu?

    Artikel:
    http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/
    http://thbecker.net/articles/rvalue_references/section_01.html

    Videos:
    http://skillsmatter.com/podcast/home/move-semanticsperfect-forwarding-and-rvalue-references
    http://channel9.msdn.com/Shows/Going+Deep/C9-Lectures-Stephan-T-Lavavej-Standard-Template-Library-STL-9-of-n



  • wxSkip schrieb:

    Shade Of Mine schrieb:

    verbieten ist immer doof. erlauben ist immer toll.

    Das hätte ich jetzt nicht von einem erfahrenen Programmierer erwartet. ⚠

    Wieso? Nachträglich etwas verbieten kann man immer. Wenn ich ein
    cout<<(a+b).invert();
    nicht mag, mach ich den return typ von op+ einfach const. Fertig.

    Aber von vornherein zu sagen, dass sowas nicht geht - ist schlecht. Es ist wichtig dass der Programmierer gewisse Freiheiten hat sich auszudrücken.

    Denn wie gesagt: gegen Murphy kann man sich auch später schützen. Schlimm ist es nur wenn die Sprachdesigner so paranoid sind, dass man nur den One-True-Way(tm) gehen kann. Und dann stellt sich heraus, dass der nicht immer der beste ist 😉 Deshalb mag ich es, wenn prinzipiell erstmal garnichts verboten wird.

    Wenn wir strenge verbote in C++ hätten, würden wir diese Diskussion ja auch garnicht führen sondern es wie in Java machen und sowieso nur Zeiger herum geben und nur darüber diskutieren ob wir jetzt eine Immutable Klassen brauchen oder eine Mutable 😉



  • @camper: Leuchtet soweit ein. Könnte man nicht so etwas wie Direktinitialisierung bei POD-Typen auch als Wert sehen? (sprich in C++11 std::array<int, 3>({{1,2,3}}) )

    @krümelkacker: Vielen Dank, werde ich mir mal in einer ruhigen Minute (also sehr bald) durchlesen. 👍

    @Shade of Mine: Wie es thenoise (vermutlich) ironisch angedeutet hat: Das C++ hätte const-correctness sicher nicht erst nachträglich eingeführt, weil dadurch Millionen existierende Codezeilen nicht mehr gültig wären :Mitleid:. Klar kann man das wegcasten - aber das könnte man bei konstanten Rückgabetypen auch.

    Shade of Mine schrieb:

    ... dass der Programmierer gewisse Freiheiten hat, sich auszudrücken ...

    Da stimme ich dir zu, aber dann solltest du das mit den "gewissen" Freiheiten nicht durch dein Absolut-Statement zunichte machen.



  • Shade Of Mine schrieb:

    Nachträglich etwas verbieten kann man immer.

    Ne, eigentlich ja andersrum. Die Abwärtskompatibilität bricht man nur äußerst ungern. Nachträglich erlauben geht immer.

    void d() &
    

    Ich hab das noch nie gesehen, hat jemand zufällig einen guten Artikel zu ref qualifiers? Ich kann auf die Schnelle nichts Vernünftiges finden.



  • fdfdg schrieb:

    ...hat jemand zufällig einen guten Artikel zu ref qualifiers? Ich kann auf die Schnelle nichts Vernünftiges finden.

    Es gibt sonst nur die offiziellen Proposals und Drafts auf open-std.org. Die Idee ist es, mit solchen Qualifizierern bei Elementfunktionen auch zwischen Lvalues und Rvalues unterscheiden zu können. "Normale" Elementfunktionen machen diesbezüglich keinen Unterschied. Die Qualifizierer beziehen sich auf *this.
    Eine Reihe von Beispielen brachte ich ja schon auf der ersten Seite dieses Threads.

    Die erste Version des Proposals:
    http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2005/n1821.htm

    Da steht die Motivation drin. Für die endgültigen Regeln guckst Du Dir am besten einen der letzten Proposals an.


Anmelden zum Antworten