D
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 das dummy)
Static local variables (oder auch block 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 über Deferred 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