Herb Sutter's "Writing Good C++14 …By Default" (PDF)



  • Wo könnte Sutters sufficiently smart static Analyzer hier warnen? Was könnte man hier hinzufügen, um dem Analyzer zu helfen?

    struct S
    {
    	void f()
    	{
    		int &i = m.front();
    		g();
    		i = 23;
    	}
    
    private:
    
    	std::vector<int> m;
    
    	void g()
    	{
    		m.emplace_back(0);
    	}
    };
    

  • Mod

    Man könnte erkennen, dass g() m modifiziert. Man kann auch mit etwas Raffinesse erkennen, dass i eine Referenz auf ein Objekt ist, dessen Lebenszeit davon abhängt, dass m nicht modifiziert wird. Demnach schlussfolgert man, dass i nach dem Aufruf ungültig sein könnte. Das ist prinzipiell machbar, erfordert aber natürlich e.g. eine vollständige Auflistung der Charakteristik aller Memberfunktionen solcher Container, usw.

    Was könnte man hier hinzufügen, um dem Analyzer zu helfen?

    Man könnte dem Analyzer mittels Attributen praktisch alles in den Schoß werfen:

    struct S 
    { 
        void f() 
        { 
            [[depends_on(m)]] int &i = m.front(); 
            g(); 
            i = 23; // error: access to potentially destroyed object
        } 
    
    private: 
    
        std::vector<int> m; 
    
        [[modifies(m)]] void g() // Das sollte ein Analyzer selbst können.
        { 
            m.emplace_back(0); 
        } 
    };
    


  • TyRoXx schrieb:

    Wo könnte Sutters sufficiently smart static Analyzer hier warnen? Was könnte man hier hinzufügen, um dem Analyzer zu helfen?

    struct S
    {
    	void f()
    	{
    		int &i = m.front();
    		g();
    		i = 23;
    	}
    
    private:
    
    	std::vector<int> m;
    
    	void g()
    	{
    		m.emplace_back(0);
    	}
    };
    

    Schönes Beispiel. Wie gesagt, ich bin mir nicht sicher, ob Sutter und co das vollständig durchdacht haben. Aber wenn das hier so ähnlich wie bei Rust laufen soll, dann müsste hier nichts annotiert werden. Der Static Analyzer würde die Funktion f ankreiden, da sich i auf etwas bezieht, was m und damit auch *this gehört, aber f danach versucht, g(); aufzurufen, welches *this als non-const-Elementfunktion ja ändern könnte und damit i invalidieren könnte. Eine funktionslokale Regel, die den Static Analyzer diesen Fehler finden lässt, könnte also so aussehen:

    Hast Du Dir etwas von X "ausgeliehen" (ein int von *this ), darfst Du solange das, worüber Du das komplette X erreichen kannst ( this ), nicht an eine andere Funktion ( g ) weitergegen.

    Und natürlich ist das eine recht pessimistische Regel. Funktionslokale Analysen müssen wahrscheinlich pessimistisch sein, wenn man 100% Speichersicherheit haben will.



  • In Rust sieht das gleiche so aus:

    struct S {
    	m: Vec<i32>
    }
    
    impl S {
    	pub fn f(&mut self) {
    		let i = &mut self.m[0];
    		self.g();
    		*i = 23;
    	}
    
    	fn g(&mut self) {
    		self.m.push(0);
    	}
    }
    

    Wenn man C++ gewohnt ist, ist das ein wenig wortreich auf den ersten Blick. Explizite Mutability ist das aber auf jeden Fall wert. Auch das explizite self erhört IMO eher die Lesbarkeit, weil man nie raten muss, ob der Bezeichner womöglich ein Member ist.

    Am besten ist aber die Fehlermeldung des Compilers, die exakt erklärt, wo das Problem ist:

    src/main.rs:17:3: 17:7 error: cannot borrow `*self` as mutable more than once at a time
    src/main.rs:17 		self.g();
                   		^~~~
    src/main.rs:16:16: 16:22 note: previous borrow of `self.m` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `self.m` until the borrow ends
    src/main.rs:16 		let i = &mut self.m[0];
                   		             ^~~~~~
    src/main.rs:19:3: 19:3 note: previous borrow ends here
    src/main.rs:15 	pub fn f(&mut self) {
    src/main.rs:16 		let i = &mut self.m[0];
    src/main.rs:17 		self.g();
    src/main.rs:18 		*i = 23;
    src/main.rs:19 	}
                   	^
    

    Das "warum" steht hier nicht drin, aber das gehört in ein Rust-Tutorial und nicht in Compiler-Output.

    krümelkacker schrieb:

    Hast Du Dir etwas von X "ausgeliehen" (ein int von *this ), darfst Du solange das, worüber Du das komplette X erreichen kannst ( this ), nicht an eine andere Funktion ( g ) weitergegen.

    Gute Idee, aber das ist keine hinreichende Bedingung. Die Regel würde viel zu viele False Positives erzeugen.

    struct S
    {
        void f()
        {
    		m.resize(1);
    		m.reserve(2);
            int &i = m.front();
            g();
            i = 23;
        }
    
    private:
    
        std::vector<int> m;
    
        void g()
        {
            m.emplace_back(0);
        }
    };
    


  • Arcoth schrieb:

    [[modifies(m)]] void g() // Das sollte ein Analyzer selbst können.
        { 
            m.emplace_back(0); 
        } 
    };
    

    Das wäre meiner Ansicht nach keine lokale Analyse mehr. Bei der Prüfung von f sollte der Analyzer nicht wissen müssen, was in g passiert. Das sollte allein von der Signatur her klar sein, was darin passieren kann. Der Benutzer darf natürlich was annotieren. Aber das soll ja nicht überhand nehmen. Wenn der Compiler das selbst heraus finden soll, dann ist die Gefahr, dass der Analyseaufwand explodiert, weil g wieder andere Funktionen aufrufen können, die wieder andere Funktionen aufrufen können und so weiter.

    TyRoXx schrieb:

    Die Regel würde viel zu viele False Positives erzeugen.

    Das meinte ich mit "pessimistisch".

    Mein Vertrauen, Speichersicherheit auf diesem Wege an C++ dranflanschen zu können, ist im Moment nicht so hoch. Die werden damit noch ihren Spaß haben…



  • krümelkacker schrieb:

    Mein Vertrauen, Speichersicherheit auf diesem Wege an C++ dranflanschen zu können, ist im Moment nicht so hoch. Die werden damit noch ihren Spaß haben…

    Bezweifel ich auch, denn bei Rust muss man ja mit ziemlichen Einschraenkungen leben, damit der Compiler den Code durchwinkt. Gerade was mehrfachen Zugriff auf unabhaengige Member von self betrifft, muss man haeufig mit type destructuring, mem::replace oder zusaetzlichen Scopes hantieren.

    Man koennte aber zumindest die Sicherheit erhoehen. Gerade array_view sollte mal in den Standard kommen, denn es nervt mich schon lange, dass es keine andere standadisierte Moeglichkeit zu pointer-Laenge-Paar und iterator-paar gibt. Natuerlich geht selbst schreiben immer, aber im Standard erhoeht es doch ziemlich die konsistenz bei mehreren libraries.



  • Das Video von Herb's Vortrag ist jetzt auch online:
    https://www.youtube.com/watch?v=hEx5DNLWGgA



  • TyRoXx schrieb:

    In Rust sieht das gleiche so aus:

    Kannst du bitte mal deinen Codeabschnitt (Compilerausgaben) auf ca. 80 Zeichen kürzen? Kein Mensch kann die erste Threadseite lesen. 😡

    Danke!


  • Mod

    Artchi schrieb:

    Kannst du bitte mal deinen Codeabschnitt (Compilerausgaben) auf ca. 80 Zeichen kürzen? Kein Mensch kann die erste Threadseite lesen. 😡

    Doch. Was für einen Browser und welche Auflösung hast du denn?



  • Artchi schrieb:

    TyRoXx schrieb:

    In Rust sieht das gleiche so aus:

    Kannst du bitte mal deinen Codeabschnitt (Compilerausgaben) auf ca. 80 Zeichen kürzen? Kein Mensch kann die erste Threadseite lesen. 😡

    Danke!

    Hab auch keine Probleme (Chrome).

    MfG SideWinder



  • krümelkacker schrieb:

    Das wäre meiner Ansicht nach keine lokale Analyse mehr. Bei der Prüfung von f sollte der Analyzer nicht wissen müssen, was in g passiert. Das sollte allein von der Signatur her klar sein, was darin passieren kann. Der Benutzer darf natürlich was annotieren. Aber das soll ja nicht überhand nehmen. Wenn der Compiler das selbst heraus finden soll, dann ist die Gefahr, dass der Analyseaufwand explodiert, weil g wieder andere Funktionen aufrufen können, die wieder andere Funktionen aufrufen können und so weiter.

    Das erscheint auf den 1. Blick sehr aufwendig.
    Ist es aber mMn. nach nicht.
    Der Compiler muss sowieso alle Funktionen (mindestens) 1x compilieren die irgendwo verwendet werden.
    Wenn er das "bottom up" macht, dann kann er alle nötigen "Annotationen" in seinen internen Datenstrukturen selbst vornehmen. Wenn man die nötigen Infos mit im .obj File abspeichert, dann kann man auch weiterhin inkrementell compilieren bzw. ist nichtmal vom Source abhängig.

    Ein kleines Problem dabei sind natürlich rekursive Funktionen (weil es da halt kein "bottom up" gibt). Das grösste Problem sehe ich aber bei Funktionszeiger/Funktoren. Denn spätestens da kommt man nicht mehr mit dem Speichern von Annotationen zu einzelnen Funktionen bzw. Typen aus. std::function<T& (U&)> kann halt alle möglichen Abhängigkeiten haben und alles mögliche modifizieren.

    Bzw. es reicht schon std::function<void()> oder ein ganz normaler Funktionszeiger, gibt ja schliesslich Lambdas bzw. bind .



  • Nachdem ich mir das Vortragsvideo angeschaut und das dazugehörige Arbeitspapier durchgelesen habe, kann ich das nur nochmal bestätigen, wie ich mir die Analyse vorgestellt habe:

    struct S
    {
    	void f()
    	{
    		int &i = m.front(); // pset(i) = { this, this' }
    		                    // bedeutet: i bezieht sich auf etwas von
    		                    // this oder was this über eine (')
    		                    // Indirektionen gehört.
    
    		g();                // g bekommt ein nicht-const this und
    		                    // kann damit alle Zeiger, die this'
    		                    // oder this'' im pset haben invalidieren.
    		                    // => pset(i) = { invalid }
    
    		i = 23;             // nicht zulässig, da pset(i) = { invalid }
    	}
    
    private:
    
    	std::vector<int> m;
    
    	void g()
    	{
    		m.emplace_back(0);
    	}
    };
    

    "pset" heißt "pointer-to set", gibt also an, wo der Zeiger hinzeigen kann und wer der Besitzer davon ist. So eine Menge kann { invalid } sein, wenn man weiß, dass der Zeiger ungültig ist oder sein kann. Wenn die Menge nicht { invalid } ist, dann enthält sie mindestens eines der folgenden: null, static, X, X', X'' (wobei X ein beliebiger Besitzer ist). Mit einem if (ptr) {...} wird für den then-Block ggf null aus dem pset rausgeschmissen. Beim Dereferenzieren wird das pset geprüft und ggf mit einer Warnung reagiert.

    So ganz ausgekocht ist das noch nicht. Der Regelsatz wird im Papier eher informell erklärt. Das Schlüsselwort mutable wird hier wohl auch noch ein bisschen Ärger machen.



  • Arcoth schrieb:

    Artchi schrieb:

    Kannst du bitte mal deinen Codeabschnitt (Compilerausgaben) auf ca. 80 Zeichen kürzen? Kein Mensch kann die erste Threadseite lesen. 😡

    Doch. Was für einen Browser und welche Auflösung hast du denn?

    Firefox und 1600x1200 Pixel.

    Aber ist klar... ist zuviel verlangt mal nen Return in der einen zu langen Zeile rein zu hauen... 🙄


Anmelden zum Antworten