At the risk of stating the obvious, TIL that constexpr constructors are not sufficient to avoid static initializers. The fact that a constructor is trivial and marked as constexpr doesn't seem to be enough to guarantee that no code will be emitted for that ctor, which becomes quite relevant in the context of an object with static storage.
Hopefully this was just me over-engineering, but since this costed me 1.5 days of debugging on three different platforms, I thought it was worth sharing some details on this list to avoid frustration to the next one trying to do something similar.
Here is what I was trying to do:
I have a struct of pods: struct Foo {int x; int y; };
I want to have a non-const pre-populated global array of Foo. In other words I want some of the first elements to be initialized to non-zero values.
I want all this to happen without initializers. As we all know, initializers (!= linker initialized data) are evil because of unpredictable ordering of execution and annex slowdowns.
So, what I wanted to achieve here was "simply" induce the compiler to put my array in the .rwdata section (and whatever windows equivalent) without generating any code.
The easiest and most obvious way to achieve this is:
struct Foo {int x; int y}
Foo g_array[100] = { {1,1}, {2,2,} /*the other 98 elements will be zero initialized*/ }
AFAICT, this still works.
Here is what I got wrong:
I didn't want |x| and |y| to be public. I meant to give out pointers to elements of the array and I didn't want clients to be able to fiddle with the raw fields.
In other words I wanted |x| and |y| to be private and I wanted to create some public accessors for them. For the sake of this toy example let's say that I wanted |x|,|y| to be increment-only.
I thought it was reasonable to do something like this and expect that it would behave exactly as above:
class Foo {
public:
constexpr Foo(int x, int y) x_(x), y_(y) {}
constexpr Foo() x_(0), y_(0) {}
int x() const { return x_; }
void IncrementX() { ++x_; }
private:
int x_; int y_;
// DISALLOW_COPY_AND_ASSIGN if you want but doesn't seem to make a difference in all this.
}
Foo g_array[100] = { {1,1}, {2,2,} }
It doesn't. This seems to hit all sort of odd behaviors and create initializers for Foo.
What does the style guide say?
At some point somebody in the bugs mentioned our code style. I gave it a re-read after the facts.
"Objects with static storage duration, including global variables, static variables, static class member variables, and function static variables, must be Plain Old Data (POD): only ints, chars, floats, or pointers, or arrays/structs of POD."
Foo in this case was a struct of POD, although with constexpr ctors. I mistakenly assumed this was still ok, it is not.
"The order in which class constructors and initializers for static variables are called is only partially specified in C++ and can even change from build to build, which can cause bugs that are difficult to find. Therefore in addition to banning globals of class type, we do not allow namespace-scope static variables to be initialized with the result of a function, unless that function (such as getenv(), or getpid()) does not itself depend on any other globals."
I think they key part that hit me is: "is a struct with a constrexpr ctor a class type?".
I genuinely thought: "it's still a struct of pods". It is not.
"Variables of class type with static storage duration are forbidden: they cause hard-to-find bugs due to indeterminate order of construction and destruction. However, such variables are allowed if they are constexpr: they have no dynamic initialization or destruction."
Note, what this is saying is that if the entire array was constexpr, i.e. constexpr Foo g_array[100] = ... this would have been fine. But this is a different thing and is not a rw array anymore.