Guter Stil in C++



  • Ich zitier mal den final draft des C99-Standards dazu:

    5.1.2.2.1 Program startup

    [#1] The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters:

    int main(void) { /* ... */ }

    or with two parameters (referred to here as argc and argv, though any names may be used, as they are local to the function in which they are declared):

    int main(int argc, char *argv[]) { /* ... */ }

    or equivalent; or in some other implementation-defined manner.

    Der FCD des C++-Standards dagegen meint:

    3.6.1 - Main function [basic.start.main]

    (...)

    -2- An implementation shall not predefine the main function. This function shall not be overloaded. It shall have a return type of type int, but otherwise its type is implementation-defined. All implementations shall allow both of the following definitions of main:

    int main() { /* ... */ }

    and

    int main(int argc, char* argv[]) { /* ... */ }

    Also: In C ist int main(void) guter Stil, in C++ ist es int main().

    Was Pointer angeht, da gehen die Meinungen weit auseinander - C-Veteranen haben sich so an Pointer gewöhnt, dass sie damit keine Fehler mehr machen (oder zu machen glauben) und dementsprechend keinen Sinn darin sehen, auf sie zu verzichten. Ich für meinen Teil vermeide Pointer, wo es geht, und verwende Referenzen. Natürlich ist eine Referenz etwas anderes als ein Pointer, aber viele Probleme, für die man in C noch Pointer benutzt hätte, lassen sich in C++ gut mit Referenzen lösen (e.g. call-by-reference *sic* oder Variablen, die von mehreren Objekten gleichzeitig benutzt werden). Das ist zwar auch nicht hundertprozentig sicher - siehe z.B.

    std::string &foo() { std::string s; return s; }
    
    // ...
    
    std::cout << foo() << std::endl;
    

    Auch wenn das wahrscheinlich meistens funktionieren wird, ist s doch nach verlassen von foo ungültig, womit auch die Referenz ungültig wird - Vorsicht ist also auch da geboten. Das ist allerdings ein Fehler, der mit einem Pointer genau so aufträte, und meines Wissens führen Refrenzen keine neuen pitfalls ein. Natürlich gibt es diverse Situationen, in denen Pointer nicht vermieden werden können, und seltener gibt es auch Situationen, in denen Pointer zwar vermeidbar wären, es aber zu umständlich wäre - tendenziell würde ich Pointer dennoch als beliebte Fehlerquelle bezeichnen. Im Endeffekt bedeuetet das: Wenn es einen offensichtlichen Weg gibt, etwas ohne Pointer zu lösen, verzichte auf Pointer.



  • 0xdeadbeef schrieb:

    Im Endeffekt bedeuetet das: Wenn es einen offensichtlichen Weg gibt, etwas ohne Pointer zu lösen, verzichte auf Pointer.

    spassvogel.

    int& allocArray(){
       return &new int[10];
    }
    

    wo pointer gemeint sind, nimm pointer.

    std::string &foo() { std::string s; return s; }
    

    jo, hier referenzen. gemeint ist ja

    std::string foo() { std::string s; return s; }
    

    und wenn man einfach nur aus performancegründen auf ne kopie verzichtet, sind referenzen das angwesagte, denn sie fühlen sich an, wie das objekt selbst und die performance-sachen kann man sich wegdenken.



  • Ob Pointer gemeint sind, oder nicht, entscheidet sich aber genau da, wo du entscheidest, ob du pointer benutzen willst, oder nicht. Wenn du einmal ein Design gebaut hast, in dem du pointer brauchst, macht es natürlich keinen Sinn, auf Pointer zu verzichten, aber davon rede ich hier nicht.



  • Ob Pointer gemeint sind, oder nicht, entscheidet sich aber genau da, wo du entscheidest, ob du pointer benutzen willst, oder nicht.

    Nein.



  • Um...doch, ich denke schon, dass sich da, wo ich entscheide, was ich meine, entscheidet, was ich meine. Oder nicht?



  • volkard schrieb:

    int& allocArray(){
       return &new int[10];
    }
    

    Versteh irgendwie nicht, was du uns damit sagen willst. Müsste das aber nicht eher so

    int** allocArray()
    {
        return &new int[10];
    }
    

    oder so

    int& allocArray()
    {
        return *new int[10];
    }
    

    aussehen?


  • Mod

    wäre nicht

    int* allocArray()
    {
        return new int[10];
    }
    

    das offensichtliche? 😕
    mal abgesehen davon, dass man das funktionsergebnis ja straflos (bis auf ein speicherloch :p ) verwerfen könnte.



  • @groove volkard wollte mit dem beispiel wahrscheinlich nur zeigen, was eine referenz alles verschleiern kann.

    die referenz verschleiert nämlich in dem beispiel gleich 2 dinge:
    1. dass es ein array ist, referenzen können nicht auf den nächsten speicherbereich inkrementiert werden(was auch irgendwie logisch ist), und somit würde kein mensch darauf kommen, dass der rückgabewert ein array ist.

    2. allokierung über new:
    new gibt einen pointer zurück den man deleten kann-auf eine referenz kann man kein delete aufrufen, darum würde es wohl sehr weit entfernt liegen zu denken, dass diese int var über new erstellt wird.

    daraus folgt: wenn ich diese funktion aus einer dll benutzen würde, würd ich kein einziges mal delete aufrufen, und kein einziges mal mehr als ein Feld des zurückgelieferten arrays benutzen, dh ich hätte einerseits ein speicherleck, aber anderereits auch ne riesige resourcenverschwendung



  • otze schrieb:

    volkard wollte mit dem beispiel wahrscheinlich nur zeigen, was eine referenz alles verschleiern kann

    Das Problem ist, das sein Code einfach falsch ist. Deshalb kann ich mir darauf keinen Reim machen.

    otze schrieb:

    referenzen können nicht auf den nächsten speicherbereich inkrementiert werden

    Mit ein bissl tricksen geht das schon.

    otze schrieb:

    new gibt einen pointer zurück den man deleten kann-auf eine referenz kann man kein delete aufrufen, darum würde es wohl sehr weit entfernt liegen zu denken, dass diese int var über new erstellt wird.

    Klingt für mich noch am logischsten. Nur stellt sich dieses Problem für micht nicht, da ich es als falsches Design empfinde, innerhalb der Funktion Speicher zu reservieren und den Clent zu nötigen, diesen ausserhalb wieder freizugeben.



  • "Stil" fängt ganz unten an: Lesbarkeit, Pflegbarkeit, ein gewisses Maß an Ordnung, und die "richtigen Tools" im kleinen.

    Also das übliche Singsang:
    - Eindeutige konsistente Bezeichner für Variablen, Funktionen etc.,
    - konsistente Einrückung, die die Ablaufstruktur verdeutlicht,
    - Kommentare die
    (1) die Schnittstelle beschreiben,
    (2) das Ziel einzelner Code-Abschnitte bekanntgeben und
    (3) über ungewöhliche bzw. auf nicht offensichtlichen Randbedingungen beruhehnde Annahmen hinweghelfen,
    - Konsistenz in den "kleinen Regeln" (NULL oder 0 für Null-Zeiger, Fehlerbehandlung über Rückgabewert oder exception, ...)

    Das Zauberwort ist Konsistenz: Für ein großes Projekt ist der gewählte Stil weniger wichtig, als ihn wirklich durchzuziehen.

    Ordnung und richtige Tools - Das geht schon fließend in Designfragen über:

    - #defines nur wenn sich die Aufgabe nicht mit Sprachmitteln lösen läßt (einschließlich "const int oder enum statt #define"),
    - längere #defines durch inline/template-Hilfsfunktionen aufbrechen,
    - Initialisierung von Variablen
    - sinnvolle Kapselung (Klassen, aber auch Trennung Interface/Implementation)
    - sinnvolle verwendung von Stadard-Patterns
    - Resource Acquisition is Initialization (RAII) - s.u.

    (RAII - innerhalb einer Implementation ist das für mich "Stil", im Interface eine Frage des Designs. Hier kommen auch die Zeiger ins Spiel: Wenn du lokal Speicher brauchst, solltest du möglichst einen sinnvollen wrapper, z.B. Array oder Smart Pointer verwenden.)



  • groovemaster schrieb:

    otze schrieb:

    volkard wollte mit dem beispiel wahrscheinlich nur zeigen, was eine referenz alles verschleiern kann

    Das Problem ist, das sein Code einfach falsch ist. Deshalb kann ich mir darauf keinen Reim machen.

    jeder kann sich mal im zeichen irren, es gibt sowas,d ass nennt sich flüchtigkeitsfehler^^

    otze schrieb:

    referenzen können nicht auf den nächsten speicherbereich inkrementiert werden

    Mit ein bissl tricksen geht das schon.

    wenn du mit tricksen pointer meisnt, dann sind das immernoch keine referenzen, du würdest nur ausweichen^^

    otze schrieb:

    new gibt einen pointer zurück den man deleten kann-auf eine referenz kann man kein delete aufrufen, darum würde es wohl sehr weit entfernt liegen zu denken, dass diese int var über new erstellt wird.

    Klingt für mich noch am logischsten. Nur stellt sich dieses Problem für micht nicht, da ich es als falsches Design empfinde, innerhalb der Funktion Speicher zu reservieren und den Clent zu nötigen, diesen ausserhalb wieder freizugeben.

    sind Fabriken ein schlechtes Design?



  • Hat ja niemand gesagt, daß eine Class Factory einen "nackten" Pointer zurückgeben muß...



  • Genau, lieber ne Referenz. 😉 🤡 😃



  • Nein, aber möglicherweise nen std::auto_ptr oder etwas in der Art. 😉



  • peterchen schrieb:

    Hat ja niemand gesagt, daß eine Class Factory einen "nackten" Pointer zurückgeben muß...

    Müssen tut man garnichts. Man kann auch ganz ohne Zeiger, Klassen und Schleifen auskommen.

    Aber Zeiger sind doch nicht schlecht? Was spricht gegen eine Factory die einen Zeiger liefert? uU mag es durchaus praktisch sein, einen Smartpointer zu liefern - aber nicht immer, weil man nicht immer einen Smartpointer braucht.

    Aber man kann ja auch mit Kanonen auf Spatzen schießen...

    btw: ha irgendjemand bei Meyers, Sutter, Stroustrup, Alexandrescu, Koenig, etc. je gelesen:
    Item XX: Avoid pointers
    Ich nicht.
    Das sollte doch schon mal Denk-Anregung sein, oder?

    Und sehen wir uns die C++ Standard Library an: iteratoren sind oft Zeiger. Weil es oft einfach sinnvoll ist. Und selbst ein echter iterator hat intern einen Zeiger - anders geht es garnicht.

    Sind iteratoren jetzt schlechter Stil?



  • Shade Of Mine schrieb:

    Müssen tut man garnichts. Man kann auch ganz ohne Zeiger, Klassen und Schleifen auskommen.

    Ja, man muß nichtmal programmieren. 😮
    Thema verfehlt...

    Shade Of Mine schrieb:

    Aber Zeiger sind doch nicht schlecht? Was spricht gegen eine Factory die einen Zeiger liefert? uU mag es durchaus praktisch sein, einen Smartpointer zu liefern - aber nicht immer, weil man nicht immer einen Smartpointer braucht.

    Aber man kann ja auch mit Kanonen auf Spatzen schießen...

    Wie paßt das jetzt genau mit Deiner sonstigen RAII-Hymne zusammen? Exception-Sicherheit?
    Einige weiter unten genannte Personen haben bereits dazu geraten stets smart_pointer zu verwenden.

    Shade Of Mine schrieb:

    btw: ha irgendjemand bei Meyers, Sutter, Stroustrup, Alexandrescu, Koenig, etc. je gelesen:
    Item XX: Avoid pointers
    Ich nicht.
    Das sollte doch schon mal Denk-Anregung sein, oder?

    jo, vor 10 Jahren gab's auch noch kaum Artikel über exception-Sicherheit. Ich will Dir inhaltlich nicht widersprechen, aber das ist einfach kein wirkliches Argument.
    Außerden im CUJ August 2004 steht: "avoid bald pointers" der Artikel ist von Hyslop und Sutter -> Denkanregung???

    Also ich meide Pointer wo es vernünftig geht, weil sie meiner Ansicht nach fehleranfälliger sind als andere Techniken. Wo man sie braucht, da braucht man sie halt, insbesondere für die Implementierung. Aber gerade aus der Schnittstelle versuche ich sie nach Möglichkeit rauszuhalten.

    MfG Jester



  • volkard schrieb:

    pointer und referenzen haben unterschiedliche bedeutungen.

    Also neulich waren Referenzen noch verkappte Pointer. 😃



  • Aber gerade aus der Schnittstelle versuche ich sie nach Möglichkeit rauszuhalten.

    Vielleicht kannst du mir ja helfen.
    Wie implementierst du die klassische Strategy-Situation?
    Du hast eine Context-Klasse die im Ctor mehrere Policy-Objekte entgegen nimmt.

    Wenn die konkreten Policy-Objekte nur innerhalb der Context-Klasse gebraucht werden, macht es ja durchaus sinn, wenn man eben diese Klasse für das Lebenszeitmanagement der Policy-Objekte verantwortlich macht. Sprich: Stirbt der Context, sterben die Policies.
    Ich reduziere mehrere jetzt mal auf zwei.
    Der Ctor nimmt also zwei Zeiger entgegen, der Client hängt bei der Konstruktion an diese zwei Zeiger mit new erzeugte Objekte.
    Soweit so gut.
    Problem:

    Context c(new Policy1, new Policy2);
    

    Ah. Da lächelt aber ein hübsches potentielles Resource-Leak.

    Hilft es wenn ich die Pointer im Interface durch Smart-Ptr ersetze? Kein Stück. Macht die Situation nur schlimmer.

    Context c(smart_ptr<Policy>(new Policy1), smart_ptr<Policy>(new Policy2));
    

    Sieht sicherer aus, hat aber das selbe Problem.

    Nächste Idee: Warum nicht einfach auf einen "virtuellen Ctor" zurückgreifen und im Interface mit Referenzen-auf-const arbeiten?

    Context c(Policy1(), Policy2());
    // in der Ctor-Implementation
    Context(const Policy& p1, const Policy& p2)
       : p1_(p1.clone())
       , p2_(p2.clone())
    {}
    

    Nachteil: Mir geht mein hübscher 0-Wert verloren. Also muss ich extra noch eine NullObj-Policy einführen. Außerdem habe ich jetzt immer zwei Konstruktionen.

    Nächste Idee: Policy-Ctoren private/protected machen und für jede Policy eine Factory-Funktion schreiben, die einen Smart-Ptr liefert.

    Context c(make_policy1(), make_policy2());
    

    Ok. Hindert aber auch niemanden daran, eigene Policies mit öffentlichem Ctor und ohne Factory-Funktion zu schreiben.

    Imo liegt hier das Problem nicht in den Zeigern im Interface sondern im Verhalten des Client-Codes.
    Hält sich der Client an einfache Regeln wie "niemals zwei dynamische Objekte in einem Ausdruck anlegen", dann gibt's auch kein Problem:

    auto_ptr<Policy> a1(new Policy1);
    auto_ptr<Policy> a2(new Policy2);
    Context c(a1.release(), a2.release());
    

    Wie löst man das Ganze effizient und so, dass selbst ein VB-Programmierer die Klasse sicher benutzen kann?



  • peterchen schrieb:

    Hat ja niemand gesagt, daß eine Class Factory einen "nackten" Pointer zurückgeben muß...

    Ich konstruier dir mal einen Fall, indem du einen nackten pointer zurückgeben musst:

    #include <vector>
    template<class T,template<class>class CreationPolicy>
    class Factory:public CreationPolicy<T>{
        public:
            Factory():CreationPolicy<T>(CreationPolicy<T>()){}
            Factory(const CreationPolicy<T>& policy):CreationPolicy<T>(policy){}
            T* create(){return CreationPolicy<T>::create();}
    };
    //policy 1: objekte haben eine längere lebensdauer als die Factory
    template<class T>
    class NewCreator{
        protected:
            T* create(){return new T();}
    };
    //policy 2: wenn die Factory zerstört wird, werden sofort alle erstellten objekte mit zerstört
    template<class T>
    class VectorCreator{
        private:
            std::vector<T*> ObjHolder;
        protected:
            T* create(){
                ObjHolder.push_back(new T());
                return ObjHolder.back();
            }
        public:
            void destroy(T* Obj){
                for(typename std::vector<T*>::iterator i=ObjHolder.begin();i<ObjHolder.end();++i){
    				if(*i==Obj){
                        delete *i;
    					ObjHolder.erase(i);
                    }
                }
            }
            ~VectorCreator(){
                for(typename std::vector<T*>::iterator i=ObjHolder.begin();i<ObjHolder.end();++i){
    				delete *i;
                }
            }
    };
    int main(){
        Factory<int,VectorCreator> Fac1;
    	int* i=Fac1.create();
    	Fac1.destroy(i);
    	Factory<int,NewCreator> Fac2;
    	i=Fac2.create();
    	delete(i);
    }
    

    da auto_ptr ihr objekt beim ableben zerstören, darf der rückgabewert von create kein auto_ptr sein, da man nicht weis, wie sich die policies verhalten.
    was bei Factory<int,NewCreator> funktionieren würde, würde Factory<int,VectorCreator> crashen lassen



  • HumeSikkins schrieb:

    Nächste Idee: Warum nicht einfach auf einen "virtuellen Ctor" zurückgreifen und im Interface mit Referenzen-auf-const arbeiten?

    Context c(Policy1(), Policy2());
    // in der Ctor-Implementation
    Context(const Policy& p1, const Policy& p2)
       : p1_(p1.clone())
       , p2_(p2.clone())
    {}
    

    Nachteil: Mir geht mein hübscher 0-Wert verloren. Also muss ich extra noch eine NullObj-Policy einführen. Außerdem habe ich jetzt immer zwei Konstruktionen.

    Den NULL-Wert kannst du an dieser Stelle eh nicht sinnvoll benutzen, weil du im Endeffekt auf eine Default-Policy zurückgreifen willst. Wenn du schon policy-basiert arbeitest, solltest du es auch richtig tun, und dann ist genau dieser Ansatz der einzig sinnvolle.

    Im Endeffekt sähe das dann so aus:

    struct foo_policy { virtual void foo() = 0; };
    struct bar_policy { virtual void bar() = 0; };
    
    struct foo_default_policy : public foo_policy { virtual void foo() { } };
    struct bar_default_policy : public bar_policy { virtual void bar() { } };
    
    class policy_user {
      foo_policy foo_p;
      bar_policy bar_p;
    
    public:
      policy_user(foo_policy const &fp = foo_default_policy(), bar_policy const &bp = bar_default_policy())
        : foo_p(fp), bar_p(bp) { }
    
      void do_something() {
        foo_p.foo();
        bar_p.bar();
      }
    };
    

    Ansonsten bist du ja ständig am überprüfen if(foo_policy == NULL) default_stuff(); else foo_policy->stuff(); - ziemlich schlechter Stil imho.


Anmelden zum Antworten