Reputation: 831
In our project we often use a construct like this (simplified for clarity, we actually use a more safe to use version):
struct Info
{
Info(int x, int y) : m_x(x), m_y(y)
{}
int m_x;
int m_y;
};
struct Data
{
static const Info M_INFO_COLLECTION[3];
};
const Info Data::M_INFO_COLLECTION[] = // global-constructors warning
{
Info(1, 2),
Info(10, 9),
Info(0, 1)
};
M_INFO_COLLECTION
can contain a large number of data points. The initialization part resides in the cpp file, which is often code-generated.
Now this structure gives us a fair amount of global-constructors
-warnings in clang across our code base. I've read in a blog post that using warnings in the -Weverything
group is a bad idea for nightly builds and I agree, and even the clang docdoes not recommend to use it.
Since the decision to switch off the warning is out of my hands, I could use a helpful trick to get rid of the warning (and a potential static initialization order fiasco), by converting the static member into a function initializing and returning a local static variable.
However, since our project does not use dynamically allocated memory as a rule, the original idea has to be used without a pointer, which can lead to deinitialization problems when my Data
class is used by other objects in a weird way.
So, long story short: The global-constructors
warning points to a piece of code that I can review as safe, since I know what the Data
class does. I could get rid of it using a workaround that can lead to problems if other classes use Data
in a particular way, but this won't generate a warning. My conclusion is that I would be better off just leaving the code as it is and ignore the warning.
So now I am stuck with a bunch of warnings, that may in some cases point to a SIOF and which I would like to address, but which get buried unter a mountain of warnings that I deliberately do not want to fix, because the fix would actually make things worse.
This brings me to my actual question: Is clang too strict in its interpretation of the warning? From my limited compiler understanding, shouldn't it be possible for the compiler to realize that in this particular case, the static member M_INFO_COLLECTION
cannot possibly lead to a SIOF, since all its dependencies are non-static?
I played around with this issue a bit and even this piece of code gets the warning:
//at global scope
int get1()
{
return 1;
}
int i = get1(); // global-constructors warning
Whereas this works fine, as I would expect:
constexpr int get1()
{
return 1;
}
int i = 1; // no warning
int j = get1(); // no warning
This looks fairly trivial to me. Am I missing something or should clang be able to suppress the warning for this example (and possibly my original example above as well)?
Upvotes: 2
Views: 1288
Reputation: 40891
The problem is that it is not constant initialized. This means that M_INFO_COLLECTION
may be zero-initialized and then dynamically initialized at run time.
Your code generates assembley to set M_INFO_COLLECTION
dynamically because of the "global constructor" (non-constant initialization): https://godbolt.org/z/45x6q6
An example where this leads to unexpected behaviour:
// data.h
struct Info
{
Info(int x, int y) : m_x(x), m_y(y)
{}
int m_x;
int m_y;
};
struct Data
{
static const Info M_INFO_COLLECTION[3];
};
// data.cpp
#include "data.h"
const Info Data::M_INFO_COLLECTION[] =
{
Info(1, 2),
Info(10, 9),
Info(0, 1)
};
// main.cpp
#include "data.h"
const int first = Data::M_INFO_COLLECTION[0].m_x;
int main() {
return first;
}
Now if you compile main.cpp
before data.cpp
, first
may access Info
out of it's lifetime. In practice, this UB just makes first
0
.
E.g.,
$ clang++ -I. main.cpp data.cpp -o test
$ ./test ; echo $?
0
$ clang++ -I. data.cpp main.cpp -o test
$ ./test ; echo $?
1
Of course, this is undefined behaviour. At -O1
, this issue disappears, and clang behaves as if M_INFO_COLLECTION
is constant initialized (as-if it reordered the dynamic initialization to before first
's dynamic initialization (and every other dynamic initialization), which it is allowed to do).
The fix to this is to not use global constructors. If your static storage duration variable is able to be constant initialized, make the relevant functions/constructors constexpr
.
If you are not able to add constexpr
s or have to use a non-constant initialized variable, then you can resolve the static initialization order fiasco without dynamic memory using placement-new
:
// data.h
struct Info
{
Info(int x, int y) : m_x(x), m_y(y)
{}
int m_x;
int m_y;
};
struct Data
{
static auto M_INFO_COLLECTION() -> const Info(&)[3];
static const Info& M_ZERO();
};
// data.cpp
#include "data.h"
#include <new>
auto Data::M_INFO_COLLECTION() -> const Info(&)[3] {
// Need proxy type for array reference
struct holder {
const Info value[3];
};
alignas(holder) static char storage[sizeof(holder)];
static auto& data = (new (storage) holder{{
Info(1, 2),
Info(10, 9),
Info(0, 1)
}})->value;
return data;
}
const Info& Data::M_ZERO() {
// Much easier for non-array types
alignas(Info) static char storage[sizeof(Info)];
static const Info& result = *new (storage) Info(0, 0);
return result;
}
Though this does have minor runtime overhead for each access, especially the very first, compared to regular static storage duration variables. It should be faster than the new T(...)
trick, since it doesn't call a memory allocation operator.
In short, it's probably best to add constexpr
to be able to constant initialize your static storage duration variables.
Upvotes: 3