C++ Stilfrage: Zugriff auf member



  • Hallo zusammen,

    wie handhabt ihr den Zugriff auf member einer Klasse in C++?

    struct SomeType
    {
       int Val;
    };
    
    class Case1
    {
       SomeType s_;
    public:
       SomeType& s()             { return s_; }
       const SomeType& s() const { return s_; }
    };
    
    class Case2
    {
       SomeType s_;
    public:
       const SomeType& s() const { return s_; }
       void set_s( const SomeType& s ) { s_ = s; }
    };
    
    class Case3
    {
    public:
       SomeType& s()             { return s_; }
       const SomeType& s() const { return s_; }
       void set_s( const SomeType& s ) { s_ = s; }
    };
    
    int main()
    {
       Case1 c1;
       c1.s().Val = 1;
    
       Case2 c2;
       SomeType s = c2.s();
       s.Val = 1;
       c2.set_s( s );
    }
    

    Variante 1 ist kürzer aber IMHO nicht so aussagekräftig wie Variante 2. Variante 3 ist iwie eine Mixtur, die sowohl die Vor- als auch Nachteile vereint. Angenommen ich möchte bei Veränderung von s_ eine Neuberechnung anstossen, dann kann ich das in set_s erledigen, ohne dass ich im weiteren Quelltext darum kümmern muss. Den non-const Zugriff müsste ich dann aber verbieten. Das mal so und mal so zu machen ist auch iwie doof, weil inkonsistent. Hat da jemand den Königsweg gefunden?



  • Dieser Thread wurde von Moderator/in SeppJ aus dem Forum Rund um die Programmierung in das Forum C++ (alle ISO-Standards) verschoben.

    Im Zweifelsfall bitte auch folgende Hinweise beachten:
    C/C++ Forum :: FAQ - Sonstiges :: Wohin mit meiner Frage?

    Dieses Posting wurde automatisch erzeugt.


  • Mod

    Keines davon. Was du hier beschreibst würde ich so machen:

    struct Case4
    {
      Sometype s;
    };
    


  • SeppJ schrieb:

    Keines davon. Was du hier beschreibst würde ich so machen:

    struct Case4
    {
      Sometype s;
    };
    

    👍



  • Das war ein Minimalbeispiel. Aber wenn ich dich richtig verstanden habe würdest du member mit Lese-/und Schreibzugriff ohne spezielle Semantik öffentlich machen. Und member mit spezieller Semantik? get/set Methode?

    #include <string>
    
    struct SomeType
    {
       int Val;
    };
    
    class Test
    {
       std::string Name_;
    
    public:
       SomeType  s;
    
       const std::string& name() const 
       {
          return Name_;
       }
    
       void set_name( const std::string& n )
       {
          check_name( n ); // Plausibilitätsprüfung
          Name_ = n;
       }
    };
    


  • Das kann man Anhand so kleiner, bedeutungfreier Beispiele nicht sinnvoll diskutieren.
    Was für eine Bedeutung hat "name"? Muss es überhaupt änderbar sein? Steht es in Verbindung mit etwas anderem, was dann mit geändert werden sollte? Bzw. allgemein: welche Invarianten hat die Klasse?
    Usw.



  • hustbaer schrieb:

    Das kann man Anhand so kleiner, bedeutungfreier Beispiele nicht sinnvoll diskutieren.

    Doch, kann man. Du steht offenbar auf dem Standpunkt, dass es vom konkreten Fall abhängt. Jetzt könntest du z.B. noch erklären, nach welchen Kriterien du diese Entscheidung triffst.

    Ein alternativer Standpunkt ist ja, immer getter/setter zu haben und nie die Variable direkt zugänglich zu machen. Dazu gibt es zumindest in einigen IDEs ja sogar automatische getter+setter-Erzeuger. Das hat unter anderem den Vorteil, dass bei späterer Änderung z.B. überall ein Check oder ein Logging eingebaut werden kann und in meinem Job-1 habe ich das in Java auch so gemacht.

    Inzwischen bzw. in C++ mache ich es aber auch vom Fall abhängig und habe insbesondere bei irgenwelchen "einfachen" Variablen keine Problem damit, wenn diese public sind. Reine Lehre, so wie man es in der Uni lernt*, ist das nicht.

    Das schlimme kommt dann noch, wenn man manchmal auch mit Delphi arbeitet und dort Properties benutzt. Das wäre dann noch ein weiterer Weg.

    Das Problem, wenn man es mal so, mal anders macht, ist, dass man sich dann schnell nicht mehr dran erinnert, wie man es gemacht hat. Aber auch bei gettern/settern ist das nicht immer klar. Manchmal wird getX/setX verwendet, manchmal X() versus X(x_new)... argh! 🤡

    *für den Fall, dass man da überhaupt irgendwas sinnvolles über Programmierung gelernt hat



  • wob schrieb:

    hustbaer schrieb:

    Das kann man Anhand so kleiner, bedeutungfreier Beispiele nicht sinnvoll diskutieren.

    Doch, kann man.

    Nein, kann man nicht.

    wob schrieb:

    Du steht offenbar auf dem Standpunkt, dass es vom konkreten Fall abhängt.

    Genau.

    wob schrieb:

    Jetzt könntest du z.B. noch erklären, nach welchen Kriterien du diese Entscheidung triffst.

    Stellst du dir einfacher vor als es ist. Natürlich kann man ein paar Dinge konkret sagen, ein paar Faustregeln gibt es schon. Diese kann ich aber nicht anhand des erfundenen bedeutungfreien Beispiels von DocShoe erklären.
    Die Frage ob das "set_name" da so OK ist, kann man nicht sinnvoll konkret beantworten.
    Die Frage wie man es "member mit spezieller Semantik" generell am besten macht kann man nicht sinnvoll konkret beantworten. Ausser vielleicht ein > 100 Seiten Buch über Programmierung zu schreiben.
    Das der Fragesteller dann aber nicht liest - TL;DR - sondern statt dessen nochmal fragt "ja aber wie nun konkret in meinem Beispiel?".

    wob schrieb:

    Ein alternativer Standpunkt ist ja, immer getter/setter zu haben und nie die Variable direkt zugänglich zu machen. Dazu gibt es zumindest in einigen IDEs ja sogar automatische getter+setter-Erzeuger. Das hat unter anderem den Vorteil, dass bei späterer Änderung z.B. überall ein Check oder ein Logging eingebaut werden kann und in meinem Job-1 habe ich das in Java auch so gemacht.

    Halte ich für Quatsch.
    Die eigentliche Frage ist nicht ob man Variablen direkt public macht oder per Getter+Setter. Direkt Variablen public ist OK für bestimmte Fälle. z.B. macht es mMn. kaum einen Sinn einer BlubProperties Klasse Getter+Setter zu spendieren, wenn sie sowieso bloss dazu dient Werte zusammenzufassen die man braucht um ein Blub zu erzeugen. Die Validierung der Werte macht man wenn man das Blub erzeugen möchte - vorher in irgendwelchen Settern irgendwas zu checken/loggen macht kaum Sinn. Und falls man es wirklich mal ändern will, trotz dem es keinen Sinn macht, dann packt man halt den Refactoring-Hammer aus, und zieht die Setter nachträglich ein.

    Achja bin schon wieder abgeschweift.
    Also. Die eigentliche Frage ist ob, für Fälle wo man nicht alles publich machen will, Getter+Setter Sinn machen. Und die Antwort darauf ist meistens: nein.

    Guck dir bloss mal das Interface von z.B. std::vector an.
    So ein vector hat z.B. ne size und ne capacity.
    Wenn es nach den Getter+Setter Fetischisten ginge, dann müsste der vector
    `getSize

    setSize

    getCapacity

    setCapacity

    haben. Hat er aber nicht. Er hatsize

    resize

    capacity

    reserveBei der Grösse ist hier lediglich der Name unterschiedlich. Was aber wichtig ist, da es Humbug istresize` als Setter anzusehen. Es ist ein Mutator. Das Ändern der Grösse bewirkt nämlich z.B. dass neue Elemente angelegt werden oder alte zerstört.

    Und bei der Capacity ist endlich der Punkt erreicht wo ein Setter einfach totaler Quatsch wäre. Weil man dem Benutzer gar nicht die Möglichkeit geben will die Capacity direkt kontrollieren zu können. z.B. könnte es gute Gründe geben auch mal mehr anzufordern als der User mit reserve anfodert. z.B. schonmal dann wenn der User reserve(1) macht, im vector aber schon mehr als 1 Element drinnen ist.

    Oder ... fstream.
    Willst du statt open(filename) ein setFilename(filename) und ein setOpen(bool) machen? Also

    fstream f;
    f.setFilename("blah");
    f.setOpen(true);
    f.seekp(123);
    f.write("x", 1);
    f.setOpen(false);
    

    Oder gleich besser

    fstream f;
    f.setFilename("blah");
    f.setOpen(true);
    f.setCursorPosition(123);
    f.setByteUnderCursor('x');
    f.setOpen(false);
    

    Und was ist mit

    fstream f;
    f.setFilename("blah");
    f.setOpen(true);
    f.setFilename("blubb"); // wird hier jetzt das File umbennannt oder ...?
    //...
    

    Das ist doch reiner Quatsch.

    Bei Klassen aus der Standard-Library verstehen das auch die meisten, und würden es vermutlich sogar total beknackt finden wenn es anders wäre. Warum also ist es so schwer das auch auf eigene Klassen anzuwenden?

    wob schrieb:

    Das schlimme kommt dann noch, wenn man manchmal auch mit Delphi arbeitet und dort Properties benutzt. Das wäre dann noch ein weiterer Weg.

    Nicht wirklich. Properties sind bloss Syntactic-Sugar für Getter+Setter.
    Wenn Properties das korrekte Mittel der Wahl sind, dann sind es in C++ eben Getter+Setter.

    wob schrieb:

    Das Problem, wenn man es mal so, mal anders macht, ist, dass man sich dann schnell nicht mehr dran erinnert, wie man es gemacht hat.

    Naja das sieht man ja im Code 😕

    wob schrieb:

    Aber auch bei gettern/settern ist das nicht immer klar. Manchmal wird getX/setX verwendet, manchmal X() versus X(x_new)... argh! 🤡

    Schreibst du Code ohne IDE? Also mir sagt meine IDE wenn ich blah. schreibe was es da an Memberfunktionen gibt. In einem > 500 KLOC Teil das aus > 100 Unterprojekten besteht.



  • Bei mir endet es eigentlich immer nur bei zwei Fällen:
    Eine Bund an Daten, wo ich nur ein struct benutze, dass falls es Memberfunktionen hat, diese nur irgendwas daraus generieren (serialisierung oder so), also nur lesen.

    Oder der Fall, dass ich eine Klasse habe, bei der das Ändern eines Members immer mehr bedeutet, also noch code ausgeführt werden muss.

    Ich habe es eigentlich nie, dass ich privates und öffentliche Variablen in einer Klasse habe. Falls es nur eine poplige Variable ist, dann klatsch ich einfach nen setter dazu.

    EDIT: Ich würde behaupten, dass man was falsch macht, wenn eine Klasse viele getter und setter hat. (Hauptsächlich viele setter)



  • Ich stimme dem prinzipiell zu.

    Auf den ersten Blick ist man geneigt, eine Ausnahme zu fordern: properties, also Paare von Getter- und Setter-Methoden, scheinen ein sinnvolles Mittel zur Abbildung von GUIs zu sein, weil der mutable state dort eine direkte visuelle Entsprechung hat (z.B. CheckBox.Checked oder TextBox.Text ). Der Setter macht dann zwar mehr als nur das Feld zuzuweisen, aber dabei handelt es sich meist nur um Notifications oder um abhängige Zustandsänderungen (z.B. andere Steuerelemente deaktivieren, wenn !Checkbox.Checked ).

    Allerdings sagt meine Erfahrung, daß diese Verwendung von properties zwar hübsch ist (insbesondere gut als Grundlage für GUI-Designer), aber wesentliche Probleme ungelöst läßt. Diese sind etwa:

    - eine Benutzereingabe soll validiert werden können, um bei falscher Eingabe eine Fehlermeldung anzuzeigen
    - ein Zustand kann eine Abhängigkeit von anderen Zuständen haben (etwa sind Steuerelemente deaktiviert, bis eine Aktion abgeschlossen ist), die man deklarativ angeben können möchte
    - es sollte möglich sein, über Zustandsänderungen benachrichtigt zu werden

    Mit einem klassischen GUI-Framework à la VCL oder WinForms geht das nur mit viel Handarbeit. Und selbst in WPF, wo alles über bindings und notifications läuft, muß man Benachrichtigungen im Property-Setter von Hand auslösen.

    Deshalb würde ich auch diesen Anwendungsfall nicht mehr mit einfachen properties abbilden wollen, sondern stattdessen sowas ähnliches wie IObservable<> verwenden. Skizze in C#:

    public interface IObservable<out T> // System.IObservable<>
    {
        IDisposable Subscribe(IObserver<T> observer);
    }
    public interface IProperty<T> : IObservable<T>
    {
        Diagnostic ValidateValue(T value);
        void SetValue(T value);
    }
    
    public static class Property
    {
        public static IProperty<T> Create() { ... } // alle Eingaben sind gültig
        public static IProperty<T> Create(Func<T, Diagnostic> validator) { ... }
    }
    
    public class Test
    {
        private Diagnostic checkName(string s) { ... }
        public IProperty<string> Name { get; } = Property.Create(checkName);
    }
    

    Das ist noch nicht besonders durchdacht (ich arbeite zurzeit nicht an GUI-Code), und für deklarative Abhängigkeiten habe ich auch noch keine Lösung, aber in die Richtung würde ich gehen.



  • Zunächst ein Mal vielen Dank für die Antworten. Ich bin zufrieden mit der Aussage, dass es von Fall zu Fall abhängt. Ausschlaggebend für die Frage war nur mein persönlicher Geschmack, ich finde den Zugriff über eine Member Funktion, die eine Referenz zurück gibt, nicht intuitiv. Auch wenn das Beispiel wieder sinnfrein ist zeigt es aber die verschiedenen Implikationen des Zugriffs.

    #include <string>
    
    class Player
    {
    public:
       // direkter Zugriff
       std::string Name;
    
       Player();
    
       int health() const 
       { 
          return Health_; 
       }
    
       void set_health( int Health )
       {
          Health_ = Health;
    
          // Teil von Variante 1
          if( Health_ < 1 )
          {
             signal_player_death( this );
          }
       }
    
       unsigned int money() const
       {
          return Money_;
       }
    
       unsigned int& money()
       {
          return Money_;
       }
    
    private:
       int          Health_;
       unsigned int Money_;
    };
    
    int main()
    {
       Player p;
    
       p.money() = 100; // <== nicht intuitiv imho
       p.set_health( 100 );
    
       // Variante 1
       int damage = calc_damage( ... );
       p.set_health( p.health() - damage );
    
       // Variante 2
       int Health = p.health() - calc_damage( ... );
       if( Health < 1 )
       {
           signal_player_death( &p );
       }
    }
    

    Mit so Sachen wie Delphi Properties bin ich schon fies auf die Nase gefallen und entferne die nach und nach. Sieht zwar hübsch aus, aber ist nicht C++ konform und wenn den Delphi Designern plötzlich einfällt das Verhalten zu ändern kann das bisher funktionierenden Code brechen.



  • DocShoe schrieb:

    // Variante 1
       int damage = calc_damage( ... );
       p.set_health( p.health() - damage );
    

    Hier könnte besser sein:

    // Variante 1
       int damage = calc_damage( ... );
       p.take_damage( damage );
    


  • ps:

    DocShoe schrieb:

    Ausschlaggebend für die Frage war nur mein persönlicher Geschmack, ich finde den Zugriff über eine Member Funktion, die eine Referenz zurück gibt, nicht intuitiv.

    Ich finde es nicht nur nicht intuitiv, ich finde es sinnlos und irreführend. Liefert man eine Referenz zurück, kann man die Variable genau so gut gleich public machen. Weil man keinerlei Kontrolle mehr über irgendwas hat. Gleichzeitig gaukelt der Funktionsaufruf aber Sicherheit/Kapselung vor.

    Was noch Sinn machen würde, wäre den "Accessor" statt der Referenz nen Proxy zurückliefern zu lassen. Das hat aber unnötigen Overhead der nicht immer wegoptimiert werden kann, und ist IMO unnötig kompliziert. Kein echter Benefit gegenüber einer normalen Setter-Funktion. Bzw. es verleitet bloss dazu Dinge als direkten Setter ( set_health ) zu machen die eben besser ein Mutator ( take_damage ) sein sollten.


Anmelden zum Antworten