Globale Variablen elegant vermeiden
-
Huhu,
mittlerweile bin ich in einer Situation angelangt, bei der mehrere Funktionen auf verschiedene Variablen zurückgreifen, die Listen, Stacks und andere Strukturen enthalten. Diese Funktionen rufen sich untereinander auf. Anfangs hatte ich dafür entsprechende Pointer global in der Bibliothek definiert:
static STACK global1; static QUEUE global2; static TOKEN global3; //etc.
Nun habe ich hier bereits mehrfach gelesen, globale Variablen sind unschön und mir gefallen diese auch nicht so wirklich. Da es jedoch mehr als drei globale Variablen sind und mehrere Funktionen habe, welche alle auf diese zugreifen, möchte ich äußerst ungern Funktionen mit 6 Übergabeparametern definieren.
Das sieht nicht wirklich schön aus.
Ein bisschen Recherche und Nachdenken hat mich zu der Überlegung gebracht, sämtliche globale Variablen in einen struct zu packen und nur diesen zu Übergeben:typedef struct { STACK global1; QUEUE global2; TOKEN global3; } GV; void init(STACK global1, QUEUE global2, TOKEN global3) { GV globale_variablen = NULL; // initialisiere GV und setze entsprechende Werte func1(globale_variablen); }
Nun zu meine(r/n) Frage(n): Ist diese Lösung akzeptabel? Wie sieht die Realität aus, in solchen Fällen? Gibt es eine elegantere Lösung?
-
Ja, ich würde auch alle benötigten Variablen in einem struct verpacken und einfach diesen übergeben.
Größere Bibliotheken verlangen auch häufig ein Context-Objekt. Wenn du deine Bibliothek dann so aufbaust, dass es entsprechende Funktionen für Initialisierung, Freigabe, usw. hast, dann hast du damit sogar ein bisschen OOP realisiert.
Context *ctx = MyLib_Init(bla, blub); if (ctx) { MyLib_DoSomething1(ctx, foo); MyLib_DoSomething2(ctx, bar); MyLib_Cleanup(ctx); }
-
Globale Variablen sind böse, das hast du richtig gelesen.
Sollte Speicher einer Funktion angehören und wiederverwendet werden, kann man diesen intern als statisch deklarieren anstatt globalen zu verwenden (löst schon einige Nachteile, aber nicht alle).
In deinem Fall allerdings soll dieser durch mehrere Funktionen beeinflusst werden, daher wäre die Weitergabe der Daten in die Funktionen die richtige Wahl. Du solltest allerdings mit Pointern zur Struktur arbeiten, da das Übergeben der Struktur selbst an die Funktionen nur eine Kopie wäre, welche teuer sein könnte und keine Veränderung der Original-Daten zur Folge hätte. Desweiteren könntest du die Felder der Struktur verstecken, dass der Inhalt nur durch die dafür zuständigen Funktionen manipuliert werden könnte.
Ein Beispiel findest du hier.
-
Youka schrieb:
Globale Variablen sind böse, das hast du richtig gelesen.
Es gibt Fälle, da sind sie ganz praktisch.
Den Context als TLS zu halten dürfte um einiges schneller sein als ihn jedesmal als Parameter zu übergeben, wodurch er sich durch den ganzen Callstack zieht.
Hat kaum praktische Nachteile, macht aber die Benutzung der Bibliothek angenehmer.
Youka schrieb:
Sollte Speicher einer Funktion angehören und wiederverwendet werden, kann man diesen intern als statisch deklarieren anstatt globalen zu verwenden (löst schon einige Nachteile, aber nicht alle).
Das löst überhaupt kein technisches Problem.
-
globi schrieb:
Youka schrieb:
Globale Variablen sind böse, das hast du richtig gelesen.
Es gibt Fälle, da sind sie ganz praktisch.
Den Context als TLS zu halten dürfte um einiges schneller sein als ihn jedesmal als Parameter zu übergeben, wodurch er sich durch den ganzen Callstack zieht.
Hat kaum praktische Nachteile, macht aber die Benutzung der Bibliothek angenehmer.
Das würde aber heißen, es dürfte nur einen Kontext geben und "um einiges schneller" würde vielleicht zutreffen, wenn er diesen Kontext sehr oft durch Funktionen schickt, ansonsten ist es nicht gewichtig.
Es gibt nur sehr selten Fälle, wo globale Variablen eine gute Entscheidung sind.SeppJ schrieb:
Globale Zustände im Programmen sind die Urzutat jedes unnachvollziehbaren Fehlers. Schon in kleinen Programmen ab 100 Zeilen blickt man kaum mehr durch. Du magst das jetzt vielleicht noch anders sehen, aber das ist wirklich die Todsünde beim Programmieren von funktionierendem und wartbaren Code. Also lern möglichst sofort, wie es richtig geht und verwende globale Variablen niemals* wieder.
globi schrieb:
Youka schrieb:
Sollte Speicher einer Funktion angehören und wiederverwendet werden, kann man diesen intern als statisch deklarieren anstatt globalen zu verwenden (löst schon einige Nachteile, aber nicht alle).
Das löst überhaupt kein technisches Problem.
Keine Überschneidung mit Variablennamen in anderen Quelldateien und Variablen stehen dort, wo sie gebraucht werden, nicht irgendwo ausserhalb der Funktion sollten Grund genug sein!
Schau mal, was du hiervon durch den Wechsel zu statischen Variablen ausschließen kannst.
-
Youka schrieb:
Globale Variablen sind böse, das hast du richtig gelesen.
Sollte Speicher einer Funktion angehören und wiederverwendet werden, kann man diesen intern als statisch deklarieren anstatt globalen zu verwenden (löst schon einige Nachteile, aber nicht alle).
In deinem Fall allerdings soll dieser durch mehrere Funktionen beeinflusst werden, daher wäre die Weitergabe der Daten in die Funktionen die richtige Wahl. Du solltest allerdings mit Pointern zur Struktur arbeiten, da das Übergeben der Struktur selbst an die Funktionen nur eine Kopie wäre, welche teuer sein könnte und keine Veränderung der Original-Daten zur Folge hätte. Desweiteren könntest du die Felder der Struktur verstecken, dass der Inhalt nur durch die dafür zuständigen Funktionen manipuliert werden könnte.
Ein Beispiel findest du hier.Genauso würde ich es auch realisieren wollen, da ich ähnliche Verfahren bereits an anderer Stelle verwendet habe.
Youka schrieb:
globi schrieb:
Youka schrieb:
Globale Variablen sind böse, das hast du richtig gelesen.
Es gibt Fälle, da sind sie ganz praktisch.
Den Context als TLS zu halten dürfte um einiges schneller sein als ihn jedesmal als Parameter zu übergeben, wodurch er sich durch den ganzen Callstack zieht.
Hat kaum praktische Nachteile, macht aber die Benutzung der Bibliothek angenehmer.
Das würde aber heißen, es dürfte nur einen Kontext geben und "um einiges schneller" würde vielleicht zutreffen, wenn er diesen Kontext sehr oft durch Funktionen schickt, ansonsten ist es nicht gewichtig.
Es gibt nur sehr selten Fälle, wo globale Variablen eine gute Entscheidung sind.In meinem Code würde ich mich gerne strikt an den C89-Standard halten, wenn TLS Sinn machten, ließe sich dieses auch dort realisieren?
Die Häufigkeit des Zugriffs würde ich höchsten mit häufig, aber nicht mit sehr häufig beschreiben.
-
Nimm dir ein Beispiel an den fxxxx-Funktionen aus stdio.h
Du bekommst von fopen ein Zeiger auf irgendetwas.
Diesen Zeiger reichst du an dei anderen Funktionen nur weiter.
Was dahinter steckt oder wo der Speicher dafür herkommt interessiert dich als Nutzer der Funktionen nicht.Wenn du es richtig machst, kannst du so auch mehrere structs in einem Programm nutzen
-
Youka schrieb:
Das würde aber heißen, es dürfte nur einen Kontext geben und "um einiges schneller" würde vielleicht zutreffen, wenn er diesen Kontext sehr oft durch Funktionen schickt, ansonsten ist es nicht gewichtig.
Ich setze natürlich voraus, dass dass es ihn wirklich nur einmal geben soll. Ich habe unten ein paar Beispiele gemacht (Bibliothekseinstellungen etc.).
Ob das im Fall des OP Sinn macht, kann ich nicht beurteilen. Aber sage bitte nicht, dass globale Variablen immer "böse" sind.
Youka schrieb:
globi schrieb:
Youka schrieb:
Sollte Speicher einer Funktion angehören und wiederverwendet werden, kann man diesen intern als statisch deklarieren anstatt globalen zu verwenden (löst schon einige Nachteile, aber nicht alle).
Das löst überhaupt kein technisches Problem.
Keine Überschneidung mit Variablennamen in anderen Quelldateien
static?
Youka schrieb:
Variablen stehen dort, wo sie gebraucht werden, nicht irgendwo ausserhalb der Funktion sollten Grund genug sein!
1 Zeile über der Funktion ist jetzt nicht wahnsinnig weit weg. Und abgesehen davon ist das kein technisches Problem. Wer static in Funktionen braucht, soll das auch verwenden, aber ich brauche öfters globale static Variablen.
Schau mal, was du hiervon durch den Wechsel zu statischen Variablen ausschließen kannst.
Disaster 1: A Local Variable Hides the Global Variable -> static und kluge Benennung lösen das
Disaster 2: Variable Name Conflicts -> der Typ kennt static echt nicht.
Disaster 3: Exposure of Implementation -> Ein Source-File sollte überschaubar sein, das muss nicht unbedingt vor sich selber gekapselt sein
Disaster 4: No Multithreading -> Atomic/TLS/Mutex/der Kontext ist auch nicht einfach so multithreaded
Disaster 5: Maximizes the Maintenance -> Blah, gleiches Argument gilt für ein Kontext-Objekt
Disaster 6: No Guarantee the Global Will Be Used -> Dafür gibts Compiler-Warnungen. Gleiches Problem übrigens für Member im Kontext-Objekt (da allerdings ohne Compiler-Warnungen)
Disaster 7:Globals Expand the Memory Footprint -> Argument: "Or worse, the global variables may be const variables defined in a header file" mit Beispiel, wie man "extern const double PI = 3.14159;" schreibt. Der Typ hat keine Ahnung.
Disaster 8: Memory for Global Variables May be Limited -> Nope, dafür ist Memory für den Callstack limitiert
Disaster 9: No Guarantee for the Order of Creation -> Klar, ein C++-Profi.
Solution1: The best course is to not have global variables in the first place. -> Klar, man braucht sie nur selten.
Solution 2: Next, you can use an anonymous namespace -> wie gesagt, static.
Solution 3: Use a Singleton. -> Ok, ich wechsle auf Java.
Allgemeine Bibliothekseinstellungen (Logging, Multithreading An/Aus, ...) mag ich z.B. als globales Kontext-Objekt. Wenn sich viele Funktionen immer das gleiche Argument übergeben, mach ich das gerne als statisches TLS Objekt (geht gut, solange keine Callbacks gemacht werden). Globale Variablen sind nicht immer böse.
-
DirkB schrieb:
Nimm dir ein Beispiel an den fxxxx-Funktionen aus stdio.h
Du bekommst von fopen ein Zeiger auf irgendetwas.
Diesen Zeiger reichst du an dei anderen Funktionen nur weiter.
Was dahinter steckt oder wo der Speicher dafür herkommt interessiert dich als Nutzer der Funktionen nicht.Wenn du es richtig machst, kannst du so auch mehrere structs in einem Programm nutzen
Hab folgendes in der stdio.h gefunden:
struct _iobuf { char *_ptr; int _cnt; char *_base; int _flag; int _file; int _charbuf; int _bufsiz; char *_tmpfname; }; typedef struct _iobuf FILE;
So stell ich mir das auch für meinen Fall vor.
globi schrieb:
Allgemeine Bibliothekseinstellungen (Logging, Multithreading An/Aus, ...) mag ich z.B. als globales Kontext-Objekt. Wenn sich viele Funktionen immer das gleiche Argument übergeben, mach ich das gerne als statisches TLS Objekt (geht gut, solange keine Callbacks gemacht werden). Globale Variablen sind nicht immer böse.
Bekanntlich führen mehrere Wege nach Rom. Grundsätzlich etwas zu verteufeln, was die Entwickler der Sprache eingeführt haben, ist vielleicht auch nicht so der richtige Weg, da die sich ja etwas bei gedacht haben.
Bisher habe ich noch nicht mit solchen statischen TLS-Objekten gearbeiten, kannst du mir vielleicht ein kurzes Beispiel geben oder wo ich mich dazu etwas besser belesen kann, wie ein solches Objekt aussehen könnte?
Es wäre für mich alleine schon deshalb interessant, um meinen Horizont zu erweitern.Bin hier bereits fündig geworden: http://gcc.gnu.org/onlinedocs/gcc-3.3.1/gcc/Thread-Local.html
Machen diese TLS-Objekte nur bei Multithreading-Anwendungen Sinn oder sind die unabhängig davon zu betrachten?
-
TLS ist in Bibliotheken eine etwas haarige Angelegenheit (klappt idR nicht, wenn diese dynamisch geladen wird). Außerdem lösen diese nicht das Reentrancy-Problem -- strtok wäre auch mit einem thread-lokalen Buffer keine hübsche Angelegenheit.
Es gibt Situationen, in denen der Gebrauch globaler Variablen vertretbar ist; das ist immer dann der Fall, wenn man mit globalem Zustand umgehen muss, der sich der eigenen Kontrolle entzieht (sei es eine schlecht geschriebene andere Bibliothek oder etwas tatsächlich globales wie Pins an einem Mikrocontroller oder std*-Streams). In aller Regel aber halte ich globis Standpunkt nicht für sinnvoll, und die Verwendung lokaler, statischer Variablen macht die Sache nicht wesentlich besser. Dabei geht es vor allem um organisatorische Gründe.
Die Hauptsache der Programmierung ist nicht, einer Maschine zu erklären, was sie tun soll. Natürlich muss das am Ende geschehen, damit ein Programm laufen kann, aber wenn man mal mit Sachverhalten umgeht, die sich nicht in wenigen tausend Zeilen Code erschlagen lassen, kommt man mit diesem Ansatz nicht weiter. Stattdessen befasst man sich die meiste Zeit damit, Probleme zu analysieren, aufzuspalten, Modelle zu ihrer Lösung zu entwickeln und dergleichen mehr -- man betreibt Organisation. Und damit das vernünftig geht, ist Verlässlichkeit in einer Schnittstelle das A und O. Man will nicht, dass der selbe Funktionsaufruf mit den selben Parametern verschiedene Ergebnisse liefern kann. Man will nicht, dass eine andere Funktion das Verhalten dritter Funktionen verändern kann, ohne, dass man etwas davon mitkriegt. Das geht nicht immer und nicht immer perfekt, aber danach zu streben lohnt sich.
Multithreading ist ein gewichtiges Argument in der einer-Maschine-erzählen-was-sie-tun-soll-Welt, und daher natürlich auch generell ernst zu nehmen, und man handelt sich mit globalen Variablen (meistens unnötig) eine neue Fehlerquelle ein, die man nicht haben will, aber aus meiner Sicht ist der Hauptgrund gegen den Umgang mit globalem Zustand (wo er sich vermeiden lässt), dass Funktionen, die von globalem Zustand abhängen, die Planung und Wartung eines Programms erheblich erschweren. Wenn du einen Kontext auf dem Call-Stack herumreichst, hast du kein Problem, wenn die Anforderungen sich ändern und mal zwei Kontexte gleichzeitig/abwechselnd gebraucht werden. Hast du den Kontext als globales Objekt irgendwo rumliegen, endet das ganze entweder damit, dass der ganze Code umgeschrieben wird (und zwar so, wie man es im ersten Anlauf gleich hätte machen sollen), oder mit wackligen Konstruktionen, in denen statt eines Funktionsparameters vor jedem Aufruf ein globales Objekt angepasst wird, um den richtigen Kontext zu haben. Alles schon erleben müssen.
Dich an FILE zu orientieren, damit bist du schon auf dem richtigen Weg. Wie genau du dein(e) Struct(s) aufziehen solltest, ist natürlich abhängig davon, was genau du vorhast; erfahrungsgemäß empfiehlt es sich, Structs grob nach Sinneinheiten zu modellieren, um sie unabhängig voneinander sinnvoll verwenden zu können. Es hindert dich nichts daran, mehrere Struct-Objekte in einem größeren Struct zusammenzupacken.
-
seldon schrieb:
TLS ist in Bibliotheken eine etwas haarige Angelegenheit (klappt idR nicht, wenn diese dynamisch geladen wird). Außerdem lösen diese nicht das Reentrancy-Problem -- strtok wäre auch mit einem thread-lokalen Buffer keine hübsche Angelegenheit.
Es gibt Situationen, in denen der Gebrauch globaler Variablen vertretbar ist; das ist immer dann der Fall, wenn man mit globalem Zustand umgehen muss, der sich der eigenen Kontrolle entzieht (sei es eine schlecht geschriebene andere Bibliothek oder etwas tatsächlich globales wie Pins an einem Mikrocontroller oder std*-Streams). In aller Regel aber halte ich globis Standpunkt nicht für sinnvoll, und die Verwendung lokaler, statischer Variablen macht die Sache nicht wesentlich besser. Dabei geht es vor allem um organisatorische Gründe.
Die Hauptsache der Programmierung ist nicht, einer Maschine zu erklären, was sie tun soll. Natürlich muss das am Ende geschehen, damit ein Programm laufen kann, aber wenn man mal mit Sachverhalten umgeht, die sich nicht in wenigen tausend Zeilen Code erschlagen lassen, kommt man mit diesem Ansatz nicht weiter. Stattdessen befasst man sich die meiste Zeit damit, Probleme zu analysieren, aufzuspalten, Modelle zu ihrer Lösung zu entwickeln und dergleichen mehr -- man betreibt Organisation. Und damit das vernünftig geht, ist Verlässlichkeit in einer Schnittstelle das A und O. Man will nicht, dass der selbe Funktionsaufruf mit den selben Parametern verschiedene Ergebnisse liefern kann. Man will nicht, dass eine andere Funktion das Verhalten dritter Funktionen verändern kann, ohne, dass man etwas davon mitkriegt. Das geht nicht immer und nicht immer perfekt, aber danach zu streben lohnt sich.
Multithreading ist ein gewichtiges Argument in der einer-Maschine-erzählen-was-sie-tun-soll-Welt, und daher natürlich auch generell ernst zu nehmen, und man handelt sich mit globalen Variablen (meistens unnötig) eine neue Fehlerquelle ein, die man nicht haben will, aber aus meiner Sicht ist der Hauptgrund gegen den Umgang mit globalem Zustand (wo er sich vermeiden lässt), dass Funktionen, die von globalem Zustand abhängen, die Planung und Wartung eines Programms erheblich erschweren. Wenn du einen Kontext auf dem Call-Stack herumreichst, hast du kein Problem, wenn die Anforderungen sich ändern und mal zwei Kontexte gleichzeitig/abwechselnd gebraucht werden. Hast du den Kontext als globales Objekt irgendwo rumliegen, endet das ganze entweder damit, dass der ganze Code umgeschrieben wird (und zwar so, wie man es im ersten Anlauf gleich hätte machen sollen), oder mit wackligen Konstruktionen, in denen statt eines Funktionsparameters vor jedem Aufruf ein globales Objekt angepasst wird, um den richtigen Kontext zu haben. Alles schon erleben müssen.
Dich an FILE zu orientieren, damit bist du schon auf dem richtigen Weg. Wie genau du dein(e) Struct(s) aufziehen solltest, ist natürlich abhängig davon, was genau du vorhast; erfahrungsgemäß empfiehlt es sich, Structs grob nach Sinneinheiten zu modellieren, um sie unabhängig voneinander sinnvoll verwenden zu können. Es hindert dich nichts daran, mehrere Struct-Objekte in einem größeren Struct zusammenzupacken.
Danke dir für die Erklärung. Finde es dennoch interessant, dass so viel in Büchern über globale Variablen stehen. Habe mich durch "The C-Programming Language" von Brian W. Kernighan und Dennis M. Richie durchgearbeitet, und diese verwenden auch recht oft globale Variablen.
-
Das wird ein Stück weit damit zusammenhängen, dass das Buch sehr alt ist -- die erste Auflage erschien 1978. Damals sah man viele Dinge ganz anders als heute, und viele Konzepte, die durch ein immer komplexeres Umfeld notwendig wurden, waren damals noch nicht bekannt oder verbreitet. Multithreading war zu der Zeit (und lange danach) beispielsweise überhaupt kein Thema.
Die Programmierung als ganzes steckte quasi noch in den Kinderschuhen. Man machte sich schon Gedanken um Programmierstil, aber solche Dinge brauchen mehr Zeit, als damals vergangen war. Ich wäre auch überrascht, wenn der Prozess schon abgeschlossen wäre und man Programmierung in 35 Jahren genau so betrachtet wie heute.