Zugriff auf ``block local static`` Objekt, während ``non-local dynamic initialization`` Phase
-
Ich habe mich die letzten Stunden ein wenig im Standard und cppreference verloren und versucht mir Gewissheit zu verschaffen, dass ich hier nichts übersehe. Deswegen ist der Titel auch etwas sperrig gerate; aber seis drum.
Im Kern geht es darum, dass ich in meiner lib bisher einige Funktionen deklariere und auch aufrufe, von denen ich erwarte, dass Nutzer diese in ihren Programmen definieren. Jetzt wollte ich das Ganze soweit optional gestalten, indem ich ein default Verhalten festlege, welches Nutzer dann überschreiben können, wenn sie es denn brauchen.
Ich habe dann ein wenig hin und her überlegt und einige Dinge ausprobiert und mich dann letztendlich für ein austauschbares global erreichbares Objekt entschieden, weil sich das für mich am robustesten anfühlt (keine Querelen mit include Reihenfolgen z.B.).
Die Idee ist es, dass dieses zentrale Objekt zu beliebiger Zeit ausgetauscht werden kann; in der Regel jedoch einmal zum Beginn des Programms. Jetzt erwarte ich nicht, dass jeder Nutzer seine eigene Implementierung erstellt und diese dort manuell einfügt. Vielmehr stelle ich Varianten bereit und Nutzer sollen über ein einfaches include, ohne dass sie es explizit selbst tun, ihre spezielle Variante auswählen können.
Hier mal ein Beispiel:foo.hpp (lib Code)
class IFoo { public: virtual ~IFoo() = default; virtual std::string name() const = 0; }; class DefaultFoo : public IFoo { public: std::string name() const override { return "DefaultFoo"; } }; [[nodiscard]] std::unique_ptr<IFoo>& get_foo() { static std::unique_ptr<IFoo> foo{ std::make_unique<DefaultFoo>() }; return foo; } template <std::derived_from<IFoo> T, typename... Args> requires std::constructible_from<T, Args...> void install_foo(Args&&... args) { get_foo() = std::make_unique<T>(std::forward<Args>(args)...); }
CustomFoo.hpp (potentieller User oder Lib Code)
#pragma once #include <Foo.hpp> class CustomFoo : public IFoo { public: std::string name() const override { return "CustomFoo"; } }; inline const int dummy = std::invoke( [] { install_foo<CustomFoo>(); return 1337; });
main.cpp (User Code)
#include <CustomFoo.hpp> int main() { std::cout << get_foo()->name(); // "CustomFoo" }
Um das bekannte "static init order fiasco" (https://en.cppreference.com/w/cpp/language/siof) zu umgehen, habe ich mir überlegt das zentrale Objekt als
block local static
Objekt zur Verfügung zu stellen. Und das führt uns endlich zur Kern-Frage:
Ist mein Vorgehen hier wohl-definiert?Ich habe versucht mich ein wenig schlau zu machen und bin dabei über, für mich relevante Termini, gestolpert:
Dynamic initialization for non-local variables
(betrifft hier dasdummy
)Static local variables
(oder auchblock local static
)
So wie ich das verstehe, passiert folgendes. Das Programm ermittelt den Speicherbedarf aller potentiell genutzten
static
Objekte und stellt den Speicher dafür bereit. Irgendwann vor main (oder auch innerhalb von main) ist garantiert, dass alle globalen Objekte fertig initialisiert wurden. Über lokale statische Objekte sagt der Standard nur, dass sie beim ersten Aufruf der Funktion initialisiert werden. Der Zeitraum dieses "ersten Aufrufs" wird vom Standard soweit ich das überblicke nicht weiter eingeschränkt, daher gehe ich davon aus, dass ich hier auch nicht in einen Problemfall laufe. Was mir noch ein wenig sorgen macht, ist der Absatz auf cppreference überDeferred dynamic initialization
(https://en.cppreference.com/w/cpp/language/initialization).It is implementation-defined whether dynamic initialization happens-before the first statement of the main function (for statics) or the initial function of the thread (for thread-locals), or deferred to happen after.
Das ist jetzt noch kein Problem. Weiter:
If the initialization of a non-inline variable(since C++17) is deferred to happen after the first statement of main/thread function, it happens before the first ODR-use of any variable with static/thread storage duration defined in the same translation unit as the variable to be initialized. If no variable or function is ODR-used from a given translation unit, the non-local variables defined in that translation unit may never be initialized (this models the behavior of an on-demand dynamic library). However, as long as anything from a translation unit is ODR-used, all non-local variables whose initialization or destruction has side effects will be initialized even if they are not used in the program.
Das klingt soweit vernünftig. Mein header muss also in einem source file eingebunden werden, in dem mindestens ein Symbol definiert wird, das auch tatsächlich im Programm genutzt wird. Wobei streng genommen in meinem Fall nicht der Constructor den Side-Effekt hat, sondern der ihn umgebende Code. Habe ich hier schon ein Problem? Das könnte man ja lösen, indem man einen dummy typen erstellt, dessen CTor den install vornimmt.If the initialization of an inline variable is deferred, it happens before the first ODR-use of that specific
Jetzt bin ich doch wieder verunsichert. Ich verstehe das so, dass die Initialisierung nur garantiert ist, wenn ich das Objekt auch wirklich benutze. Aber ich nutze es nie, sondern es ist lediglich ein Hilfsmittel um den Install Prozess anzustoßen. Aber wahrscheinlich greift hier wieder das "Side-Effekt" Argument.Ich hoffe, irgendjemand kann mir diesen gedanklichen Knoten lösen
Gruß, Dominic
-
Backticks (erst recht nicht doppelte oder vierfache) gehören nicht in die Überschrift.
-
@DNKpp sagte in Zugriff auf ``block local static`` Objekt, während ``non-local dynamic initialization`` Phase:
Im Kern geht es darum, dass ich in meiner lib bisher einige Funktionen deklariere und auch aufrufe, von denen ich erwarte, dass Nutzer diese in ihren Programmen definieren. Jetzt wollte ich das Ganze soweit optional gestalten, indem ich ein default Verhalten festlege, welches Nutzer dann überschreiben können, wenn sie es denn brauchen.
Sozusagen ein Interface. In meinem Web-Application-Framework (Perl) habe ist das so gelöst, daß vor dem Ausführen dieser Methoden geprüft wird, ob sie in der Subklasse definiert sind. In meiner Basisklasse sind diese Methoden gar nicht definiert, also auch kein Default-Verhalten. Evntl. kann man das auch mit c++ so machen.
Mfg
-
@DNKpp Hm, ich bin mir jetzt gerade auch nicht sicher ob dein Fall hier wohldefiniert ist.
Aber ich bin kein Freund von dem Design Ansatz mit dem Dummy Objekt. Was spricht dagegen, wenn man das zentrale Objekt austauschen will, direkt die install Funktion aufzurufen?
Hintergrund ist, was ist, wenn es mehrere Custom Foos gibt, die jeweils so ein Dummy Objekt initialisieren. In welcher Reihenfolge werden die dann initialisiert und welches Objekt lebt am Ende im unique ptr?
-
@Schlangenmensch sagte in Zugriff auf ``block local static`` Objekt, während ``non-local dynamic initialization`` Phase:
@DNKpp Hm, ich bin mir jetzt gerade auch nicht sicher ob dein Fall hier wohldefiniert ist.
Aber ich bin kein Freund von dem Design Ansatz mit dem Dummy Objekt. Was spricht dagegen, wenn man das zentrale Objekt austauschen will, direkt die install Funktion aufzurufen?Gar nichts, kann man machen. Nur möchte ich halt Alternativen bereitstellen, die Nutzer bequem ins Programm einbinden können, ohne selbst von diesem install Prozess überhaupt wissen zu müssen.
Ganz konkret geht es mir darum, dass meine lib mit unterschiedlichen third-party unit-test frameworks zusammenspielen soll. Man kann dabei davon auszugehen, dass immer nur eine der Möglichkeiten im Programm genutzt wird. Also im Endeffekt ist dieses ganze Konstrukt eine bridge, um diese Frameworks über gewisse Sachverhalte zu informieren (Fehler, Tracing, etc.).
Wenn mehrere genutzt werden, dann bleibt nichts anderes übrig, als in diesen Prozess doch manuell einzugreifen. Dadurch, das es durchgehend austauschbar ist, limitiere ich diesen Fall nicht aber direkt supporten möchte ich ihn auch nicht.@Schlangenmensch sagte in Zugriff auf ``block local static`` Objekt, während ``non-local dynamic initialization`` Phase:
Hintergrund ist, was ist, wenn es mehrere Custom Foos gibt, die jeweils so ein Dummy Objekt initialisieren. In welcher Reihenfolge werden die dann initialisiert und welches Objekt lebt am Ende im unique ptr?
Das ist dann wohl klassisches Undefined Behavior und ich wäre auch zufrieden damit, das so zu benennen. Zumindest, wenn nicht manuell eingegriffen wird. Denn, austauschen lässt es sich ja ohne Probleme.