Pitfalls C++11



  • Und sonst fällt mir gerade kein "Pitfall" mehr ein.

    Aber ein C++98/03 Pitfall waren ja die vom Compiler generierten Kopierkonstruktoren und Zuweisungsoperatoren. Da sind einfach viele Anfänger reingelaufen. Die Sache scheint sich mit C++11 entschärft zu haben, da ja die automatische Erzeugung von diesen Operationen, wenn z.B. ein benutzerdefinierter Destruktor vorhanden ist, deprecated ist und es wahrscheinlich wenigstens zu einer Warnung kommen wird. 🙂



  • Typinferenz
    Wie angetönt, siehe auch hier.

    Die "einheitliche" Initialisierung und Initialisiererlisten
    Hier hat man meiner Meinung nach systematisch gepfuscht, indem man alles erlaubt und das entstehende Chaos als einheitlich bezeichnet hat. Durch die syntaktische Mehrfachbelegung können Fehlinterpretationen entstehen.

    class IntVector
    {
        public:
            MyVector(std::size_t size, int value);
    };
    
    void Function(IntVector x);
    
    // Da ich cool bin, verwende ich auch die coole Initialisierung für 3-elementige Vektoren
    Function({3, 5});
    // Ruft Konstruktor MyVector(std::size_t, int) auf
    
    // Der MyVector-Entwickler will auch cool sein und rüstet seine Klasse auf:
    class IntVector
    {
        public:
            MyVector(std::size_t size, int value);
            MyVector(std::initializer_list<int> values);
    };
    
    // Gleiche Anwendung
    Function({3, 5});
    // Ruft Konstruktor MyVector(std::initializer_list<int>) auf
    // -> Code-Break ohne Compilerfehler oder -warnung
    

    RValue-Referenzen und Templates
    Sorgt garantiert massiv für Verwirrung, aber ist für Perfect Forwarding notwendig. Der Fallstrick hat echt Potential.

    void IntFunction(int&& x)
    {
       Change(x); // kein Problem, x ist eh temporär
    }
    
    // Ah, wir können das ja verallgemeinern:
    template <typename T>
    void TFunction(T&& x)
    {
        Change(x); // Problem, x kann irgendwas sein
    }
    
    int lvalue;
    IntFunction(lvalue); // Compilerfehler, Funktion ändert nicht aus Versehen Wert
    TFunction(lvalue); // kein Compilerfehler, Parameter wird als int& (nicht int&&) hergeleitet
    

    Move-Semantik
    Wobei ich das Verhalten weniger praxisrelevant finde als bei den oberen Beispielen, aber ist mir noch eingefallen (original von Meyers oder so).

    struct DynamicArray
    {
        std::vector<int> vec;
        std::size_t size;
    };
    
    DynamicArray x;
    x.vec.resize(20);
    x.size = 20;
    
    DynamicArray y = std::move(x);
    // x.size == 20
    // x.vec.size() == 0
    // Invarianten durch impliziten Move-Konstruktor verletzt
    

    Initialisierung von Membervariablen in Klassendefinition
    Ich hoffe, mein Wissen diesbezüglich ist noch aktuell. Falls ja, könnte sowas passieren (gekünsteltes Beispiel, mir fällt gerade kein sinnvolles ein). Sehe ich aber auch nicht als allzu grosse Gefahr.

    class MyClass
    {
        int member;
        public:
            MyClass();
    };
    
    MyClass::MyClass() : member(72) {}
    
    // Code wird auf C++11 portiert, Änderung der Konstruktordefinition wird vergessen
    class MyClass
    {
        int member = 72;
        public:
            MyClass();
    };
    
    // Später wird anderer Wert eingesetzt, doch Konstruktor schreibt nach wie vor 72
    class MyClass
    {
        int member = 80;
        public:
            MyClass();
    };
    


  • Nexus! Ja, dein Beispiel kann wirklich üble Auswirkung haben. 😮
    Aber vielleicht sollte man sich einfach daran gewöhnen Interface-Typen genauer zu definieren.

    Ein Vorschlag für den IntVector-Designer:

    struct Size
    {
        std::size_t m_size;
        Size(std::size_t s) : m_size(s) {};
    };
    
    class IntVector 
    { 
        public: 
            IntVector(Size size, int value); 
            IntVector(std::initializer_list<int> values); 
    };
    void Function(IntVector x);
    

    Dann gibt es keine Verwechslung mehr.



  • Tachyon schrieb:

    314159265358979 schrieb:

    Das kann aber schon mein Compiler, dazu brauche ich keine Expression Templates. 🙄

    Guck Dir an, wie man es wirklich macht. Ich schätze mal, dass zum Beispiel boost.ublas oder spirit da passenden Code bereithalten. Alternativ guck Dir nochmal irgendwelche Idiomseiten am, die Expressiontemplates behandeln.

    Typischweise kann man übrigens auch bei Implementierungen die Expressiontemplates benutzen std::cout << (a + b) schreiben, und, Geheimtip, es wird dabei sinnigerweise nicht extra ein operator<< für die Operation überladen.
    Auch sowas wie a + b + c dürfte bei Deiner Implementierung schwierig werden.

    Dann zeich mich mal wie man das Beispiel hier mit Expression-Tempaltes macht, und die Operatoren trotzdem den konkreten Typ zurückgeben lässt:
    http://en.wikipedia.org/wiki/Expression_templates

    (EDIT: und ich meine *nicht* statt x = alpha * (u-v) einfach x = multiplication(alpha, subtraction(u, v)) schreiben. Ob ich jetzt "+" oder "addition" schreibe ist ja letztlich egal.)

    @Pi
    So lange was nicht compiliert, und es sich einfach fixen lässt, sehe ich kein wirkliches Problem.
    Fix:

    //      auto c = a + b;
            auto c = evaluate(a + b);
    
            std::swap(a, c);
    

    Blöd werden erst Fälle wo man keinen Compiler-Error bekommt, aber falsches Verhalten (oder Code-Bloat auf Grund von tausenden sinnfreien Template-Instanzierungen).



  • Artchi schrieb:

    Nexus! Ja, dein Beispiel kann wirklich üble Auswirkung haben. 😮
    Aber vielleicht sollte man sich einfach daran gewöhnen Interface-Typen genauer zu definieren.

    Ein Vorschlag für den IntVector-Designer:

    struct Size
    {
        std::size_t m_size;
        Size(std::size_t s) : m_size(s) {};
    };
    
    class IntVector 
    { 
        public: 
            IntVector(Size size, int value); 
            IntVector(std::initializer_list<int> values); 
    };
    void Function(IntVector x);
    

    Dann gibt es keine Verwechslung mehr.

    Juchu, wir haben eine komplizierte Sprache mit viel Glatteis noch komplizierter und rutschiger gemacht!!! 😕

    Ne, sorry, ich hoffe echt du meinst das als Scherz.



  • Was ist an einem weiteren Datentyp bitte kompliziert? Das ist einfach nur ein genauerer Typ als es mit size_t versucht wurde.

    Wenn dir weitere Typen kopfzerbrechen bereiten, dann ist das ja dein Problem.



  • Artchi schrieb:

    Was ist an einem weiteren Datentyp bitte kompliziert? Das ist einfach nur ein genauerer Typ als es mit size_t versucht wurde.

    Wenn dir weitere Typen kopfzerbrechen bereiten, dann ist das ja dein Problem.

    Was ist daran nicht kompliziert?
    Und vor allem: wie "entschärft" es den von Nexus beschriebenen Stolperstein?



  • @hustbaer: lies alle meine Beträge. Ich schrieb schon was zum englischen Wiki-Beitrag.



  • Tachyon schrieb:

    Guck Dir an, wie man es wirklich macht. Ich schätze mal, dass zum Beispiel boost.ublas oder spirit da passenden Code bereithalten.

    uBLAs als auch sein schneller counterpart Eigen, sowie eigentlich alle modernen Bibliotheken für lineare Algebra geben die Operation zurück, und kein Temporary.

    Das hat midnestens zwei Gründe:

    1. RVO kriegt nicht alle Kopien entfernt schon bei Ausdrücken wie:

    x = a+(b+c)

    muss der Compiler ziemlich schlau sein um sich zu merken, dass er die zurückgegebene Kopie von b+c wiederverwenden kann. Du endest also damit, dich bei jedem Ausdruck selbstständig um die Kopien zu kümmern.

    2. Es ist klüger, aus a+(b+c) eine Metaoperation zu machen. Die Operation kann direkt so gebastelt werden, dass der Compiler weniger Probleme hat, sie gut zu optimieren.

    Typischweise kann man übrigens auch bei Implementierungen die Expressiontemplates benutzen std::cout << (a + b) schreiben, und, Geheimtip, es wird dabei sinnigerweise nicht extra ein operator<< für die Operation überladen.

    uBLAS hat meines Wissens nach einen operator<< überladen, und zwar mit ublas::vector_expression<T> als Argument(das ist ihre Basisklasse für vektor-expression templates)



  • Nexus schrieb:

    Die "einheitliche" Initialisierung und Initialisiererlisten

    Wer den normalen Konstruktor dann mit {} aufruft ist ja mal ganz hart selbst schuld 😉

    Nexus schrieb:

    RValue-Referenzen und Templates

    Ok, aber irgendwie auch unrealistisch, aber man nutzt doch nicht plötzlich rvalue Referenzen wenn man eine Kopie haben will. Also auch nicht in dem ersten int Beispiel. Die nimmt man nur in Konstruktoren/operator =, oder wenn man auch wirklich forwarden will. Insofern sehe ich das Problem auch hier nicht wirklich.

    Nexus schrieb:

    Move-Semantik

    Wenn sich alle auf eine Bedeutung von move einigen würden, wäre die Sache geritzt: http://www.c-plusplus.net/forum/300248 😃

    Nexus schrieb:

    Initialisierung von Membervariablen in Klassendefinition

    Tjoa in der Tat, allerdings würde ich wegen Übergangsfehlern nicht auf ein Feature verzichten wollen.



  • Tachyon schrieb:

    @hustbaer: lies alle meine Beträge. Ich schrieb schon was zum englischen Wiki-Beitrag.

    Ja OK, hab ich jetzt.
    Was ich nicht verstehe, bzw. für extrem ungünstig halte, sind deine Formulierungen hier.

    Liest sich einfach alles so, als wolltest du sagen dass man Expression-Templates *machen* kann, ohne jemals irgendwo ne Operation zurückzugeben. Und das ist natürlich vollkommener Quatsch.

    z.B. die Aussage hier

    Und seine Aussage, dass die Rückgabe des konkreten Typs des Operanden die Idee hinter Expression Templates ad absurdum führt, ist einfach falsch.

    Entweder hast du da nicht verstanden was Pi gemeint hat (nämlich genau das was ich oben geschrieben habe: irgendwer muss irgendwo ne Operation zurückgeben, denn das ist es was Expression Templates ausmacht). Oder aber ... ja weiss nicht.
    Klar muss man nicht Operatoren nicht mit Expression Templates machen. Nur dann haben sie eben auch genau gar nix mit Expression Templates zu tun. Ob sie intern welche verwenden oder nicht ist dann ja vollkommen egal.

    Naja, Wurst, es gibt wichtigeres 🙂



  • cooky451 schrieb:

    Wer den normalen Konstruktor dann mit {} aufruft ist ja mal ganz hart selbst schuld 😉

    Was hälst du in diesem Kontext von Stroustrup der in all seinen Codebeispielen aggressiv die Konstruktoren durch {}-Notation ersetzt?



  • otze schrieb:

    Was hälst du in diesem Kontext von Stroustrup der in all seinen Codebeispielen aggressiv die Konstruktoren durch {}-Notation ersetzt?

    Für structs / initializer_lists ist das ok. Andere Beispiele habe ich noch nicht gesehen - und würde es auch sehr komisch finden.



  • hustbaer schrieb:

    Liest sich einfach alles so, als wolltest du sagen dass man Expression-Templates *machen* kann, ohne jemals irgendwo ne Operation zurückzugeben. Und das ist natürlich vollkommener Quatsch.

    Hast ja recht.



  • Artchi schrieb:

    Ein Vorschlag für den IntVector-Designer:

    struct Size
    {
        std::size_t m_size;
        Size(std::size_t s) : m_size(s) {};
    };
    

    Der Typ ist alleine schon deswegen unpraktisch, weil man ihn nicht als Array-Index verwenden kann. Ausserdem gibts meiner Meinung nach bereits jetzt zu viele Typen für Grössenangaben.

    cooky451 schrieb:

    Wer den normalen Konstruktor dann mit {} aufruft ist ja mal ganz hart selbst schuld 😉

    Wenn die Sprache diese Möglichkeit vorsieht, wird sie mit Sicherheit auch benutzt. Ist ja nicht so, dass die Problematik einem gleich auf Anhieb ins Auge sticht.

    Es handelt sich um ein generelles Problem bei neuen Features: Bis sich gängige Idiome und vernünftige Stile in der C++11-Community etablieren, werden einige Jahre ins Land ziehen -- sofern dies überhaupt jemals passiert, zumal wohl viele Leute weiterhin bei C++98 bleiben. Etwas schade finde ich, dass sich bei der Initialisierungssyntax aufgrund der vielen Möglichkeiten Stile bilden müssen, um konsistent zu programmieren. Schöner wäre es, wenn es eine in der Sprache vorgesehene Möglichkeit gäbe; oder selbst bei mehreren Möglichkeiten ein Weg der offiziell empfohlene wäre (überall {} kann nicht sinnvoll sein). Und weil das eben nicht so ist, werden erneut etliche inkompatible Konventionen entstehen.

    cooky451 schrieb:

    Ok, aber irgendwie auch unrealistisch, aber man nutzt doch nicht plötzlich rvalue Referenzen wenn man eine Kopie haben will.

    Warum? Es kann ja sein, dass ich eine Überladung mit const MyClass& und eine mit MyClass&& anbiete, wobei letztere durch Move-Semantik optimiert ist. Verallgemeinere ich das auf const T& und T&& , wird plötzlich nur noch die T&& -Überladung aufgerufen, wodurch versehentlich LValues verändert werden können.

    cooky451 schrieb:

    Tjoa in der Tat, allerdings würde ich wegen Übergangsfehlern nicht auf ein Feature verzichten wollen.

    Klar, aber es geht ja hier um Pitfalls. Und letztere passieren zum Grossteil beim Übergang zu C++11, weil Entwickler nicht mit neuen Features vertraut sind.

    Wobei ich auch sagen muss, dass ich von der Initialisierung in der Klassendefinition noch nicht ganz überzeugt bin... Muss ich mal schauen, wie sich das in der Praxis bewährt.



  • Nexus schrieb:

    Es kann ja sein, dass ich eine Überladung mit const MyClass& und eine mit MyClass&& anbiete

    Eben nicht, das war der Punkt. Ich gehe üblicherweise davon aus, dass move Kosten gegen 0 gehen und erwarte Parameter per value*. Dann hat man alles mit einer Klappe geschlagen, indem man einfach aussagt "ich will kopieren". Wenn man also einfach nicht davon ausgeht, dass man bei irgendeiner nonconst Referenz sicher vor Veränderungen wäre, hat man auch kein Problem. 😉

    Allerdings stimme ich dir in dem Punkt zu, dass es immer schwieriger wird einen durch die gesamte Community akzeptierten Stil aufzubauen, da keine alten Features aus der Sprache gestrichen werden.

    * Wenn ich weiß dass ich kopieren will, gilt natürlich nicht für reine forward Funktionen wie make_unique etc.



  • Ich sehe das Problem mit der {}-Initialisierung nicht wirklich, Unstimmigkeiten gibt es lediglich bei Containerklassen.
    Bei Code wie

    MyComplex c{0, 1};
    

    ist intuitiv klar, was passiert, ohne dass MyComplex irgendeinen Konstruktor mit initializer_list anbieten muss. Entsprechend andere sinnvolle Beispiele lassen sich finden (pair, tuple, alle Arten von Datenstructs, Vektoren/Matrizen, … - eben alles, was Daten speichert, aber nicht wirklich ein Container ist), insofern macht die {}-Syntax für normale Konstruktoren durchaus Sinn.

    Bei Containerklassen ist die Lage ähnlich. Bei

    MyVector v{0, 1};
    

    ist für mich auch intuitiv logisch, was passiert, nämlich dass der Vector mit den Elementen 0 und 1 initialisiert wird. Wenn das nicht funktioniert, sondern der (Elem, Size)-Konstruktor aufgerufen wird, hat der Designer geschlampt. Ich würde gar nicht erwarten, dass dieser bei einem Container benutzt wird, geschweige denn würde ich Code schreiben, der sich darauf verlässt. Dazu gibt es die altmodische Syntax.

    Insofern ist ein Pitfall bei Containerklassen und ähnlichen Typen vorhanden, ich sehe das aber weit weniger dramatisch, als es hier dargestellt wird.


Anmelden zum Antworten