Quick context: I'm seeing errors on program shutdown, that stem from dependencies between global members (::sigh::, I know, I know). One global variable's destructor might refer to another global -- and if that one's already been destructed, things get bad.
But here's a particular case where I just don't know whether behavior is well-defined: a static variable inside a function. Can I rely on the function behaving consistently even during program shutdown? Or is it possible that the static member will be destroyed, and the function will run anyway, without creating a new one?
Here's a toy example demonstrating what I'm interested in:
class Logger
{
public:
enum class Severity { DEBUG, INFO, WARNING, ERROR };
void Log(Severity sev, const std::string& msg)
{
LogImpl(FormatMessage(sev, msg));
}
Logger() { Log(Severity::INFO, "Logger active"); }
~Logger() { Log(Severity::INFO, "Logger inactive"); }
protected:
static std::string FormatMessage(Severity sev, const std::string& msg)
{
static const std::map<Severity, std::string> enum2str {
{Severity::DEBUG, "DEBUG"},
{Severity::INFO, "INFO"},
{Severity::WARNING, "WARNING"},
{Severity::ERROR, "ERROR"}
};
// Throws or crashes if enum2str is invalid, or uninitialized:
return "[" + enum2str[sev] + "] " + msg;
}
void LogImpl(const std::string& msg)
{
std::cout << msg << std::endl;
}
};
Let's imagine I have a global instance of Logger
. The enum2str
map in Logger::FormatMessage
is a static variable, so at some point during program shutdown, it will be destroyed.
By standard, can this cause my program to crash on shutdown? Is enum2str
inherently unreliable during shutdown? Or is there some handling of this -- for example, if enum2str
is invalid at some point, perhaps a new static instance will be created?
(I am not interested in relying on destruction order between objects, e.g. where I declare the global Logger
instance.)
Yes. Just for a little more context, static data is created and placed in your applications Stack Data segment (memory) and will remain there for the life time of the application. You can always reference it and do not need to worry about it being deallocated (unless it is an optional and you nil it out).
Static variables in C++ have program lifetime thus they will not go out of scope so no race conditions. However, this function can only be called by a single thread at a time since the static variables are shared over all invocations of the function which may not be a suitable solution.
Static variables in methods i.e. you cannot use a local variable outside the current method which contradicts with the definition of class/static variable. Therefore, declaring a static variable inside a method makes no sense, if you still try to do so, a compile time error will be generated.
Initialization of static variables A static variable in a block is initialized only one time, prior to program execution, whereas an auto variable that has an initializer is initialized every time it comes into existence.
Without seeing more of your program, the general answer is yes. The destruction of that static map can cause your program to have undefined behavior:
[basic.start.term]
3 If the completion of the constructor or dynamic initialization of an object with static storage duration strongly happens before that of another, the completion of the destructor of the second is sequenced before the initiation of the destructor of the first [...]
4 If a function contains a block-scope object of static or thread storage duration that has been destroyed and the function is called during the destruction of an object with static or thread storage duration, the program has undefined behavior if the flow of control passes through the definition of the previously destroyed block-scope object. Likewise, the behavior is undefined if the block-scope object is used indirectly (i.e., through a pointer) after its destruction.
In general, static objects are destroyed in reverse order to being initialized. So hypothetically, if you have a static object that is initialized early, before the map in the logger, and it logs something in its own destructor, you'll get undefined behavior.
I am not interested in relying on destruction order between objects
You should, since that is exactly what determines whether FormatMessage
is safe to call during the program shutdown.
The code that runs during shutdown is destructors of static objects, and function registered with atExit
.
Can I rely on a function-scoped static variable for a method called during program shutdown?
You cannot rely on it in general, but you can rely on it in certain circumstances.
It is safe to rely on static objects in atExit
, so it is safe to call FormatMessage
there. Unless you can guarantee the order of destruction between particular static object s
, and enum2str
, it is not safe to use FormatMessage
in the destructor of s
.
Static objects are guaranteed to be destroyed in reverse order of their construction. Therefore you can rely on enum2str
existing during destruction of the subset of static objects whose constructors call FormatMessage
, because calling FormatMessage
in the constructor ensures that enum2str
will have been constructed before that dependent static object has finished constructing.
There is a trick to relying on a static object regardless of the order of destruction: Never destruct the dependee. This can be achieved by using a static function scope pointer to a dynamically allocated object, that you intentionally never delete. As a drawback, this will trigger diagnostics in memory analyzers and may increase the blood pressure of your dogmatic co-workers.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With