New issue
Advanced search Search tips
Note: Color blocks (like or ) mean that a user may not be available. Tooltip shows the reason.

Issue 660967 link

Starred by 2 users

Issue metadata

Status: WontFix
Owner: ----
Closed: Nov 2016
Cc:
Components:
EstimatedDays: ----
NextAction: ----
OS: ----
Pri: 3
Type: Bug



Sign in to add a comment

global objects + constexpr constructor == evil

Project Member Reported by primiano@chromium.org, Oct 31 2016

Issue description

At a first glance, this might seem similar to Issue 660828, but it's not.
This time I removed the birds, there are no volatile involved. 
Also this time MSVC is the one misbehaving (whatever version was pulled by chromium's corp depot_tools today).

Given this code:
---
extern "C" int printf(const char *, ...);

struct A {
  constexpr A() : ignored(0), v(42) {}
  constexpr A(int v) : ignored(0), v(v) {}
  int ignored;
  int v;
};

struct B {
  int ignored;
  int v;
};

bool Initializer();

bool init = Initializer();
A a[10] = { {/*v=*/1} };
B b[10] = { {/*ignored=*/0, /*v=*/1} };

bool Initializer() {
  printf("In initializer, before changing: a[1].v=%d b[1].v=%d\n", a[1].v, b[1].v);
  a[1].v = 666;
  b[1].v = 666;
  printf("In initializer, after changing:  a[1].v=%d b[1].v=%d\n", a[1].v, b[1].v);
  return true;
}


int main() {
  printf("In main, after changing:         a[1].v=%d b[1].v=%d\n", a[1].v, b[1].v);
  return 0;
}
---

On GCC and Clang this prints, as one would expect:
---
In initializer, before changing: a[1].v=42 b[1].v=0
In initializer, after changing:  a[1].v=666 b[1].v=666
In main, after changing:         a[1].v=666 b[1].v=666
---


On MSVC this prints:
---
In initializer, before changing: a[1].v=0 b[1].v=0        <-- !!! a[1].v = 0, would have expected 42
In initializer, after changing:  a[1].v=666 b[1].v=666
In main, after changing:         a[1].v=42 b[1].v=666     <-- !!! a[1].v = 42, ... now?
---

In other words, in the case of MSVC the 0-arg constexpr ctor runs *after* Initializer(). However, funny, the 1-arg constexpr ctor has effect *before* Initializer(). So looks like the first element of |a| gets linker initialized, but not the remaining ones, which get initialized even after the explicit Initializer().

Want more fun? If you remove the |ignored| argument everything behaves sanely. For some definition of sanely. I don't know what to expect anymore. I will never use constexpr constructors + global objects again. This is what I learned today.

+brucedawson: not sure if this is a MSVC compiler bug and if/how a bug should be filed.
 
I am less surprised by this than you are. Disappointed, perhaps, but not entirely surprised.

> So looks like the first element of |a| gets linker initialized, but not
> the remaining ones, which get initialized even after the explicit Initializer().

Yes. If you mark 'A' as constexpr (and remove the writes to it) then all of it is linker initialized, as required by constexpr. Is it possible to do that in the real version of this code?

But what you have right now is just classic global value order of initialization fiasco fun. A has a constructor, because it can. That constructor runs after the constructor for 'init'. And yeah, part of 'A' is linker initialized, part isn't, but I don't think that's a bug.

Avoid global variables that are not constexpr. Initializing non-constexpr variables with constexpr expressions is not the same thing. The constexpr on the constructors is a red herring. It has no effect on the guarantees or on the results.

Comment 2 by h...@chromium.org, Oct 31 2016

> It has no effect on the guarantees or on the results.

I'm not sure that's strictly true (but I'd need to consult my C++ lawyer), but I do think you're in that dodgy C++ territory where things are uncertain and it's safer to just stay away :-)

Or as the style guide says: "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."
Let's put it this way. Not entirely surprised by the fact that the constexpr ctor that I was hoping to become a linker-initialized thing turns still into a real initializer.
Instead, quite surprise by the inconsistency, the fact that one of the two constepr ctor (the 1-arg one) comes before and the other (0 arg) comes after. This one still makes very little sense to me. Although, agree with the general rule that once you get into the static initialisers land trying to reason on ordering is a dead cause.

> Yes. If you mark 'A' as constexpr (and remove the writes to it) then all of it is linker initialized, as required by constexpr. Is it possible to do that in the real version of this code?

Yeah I see what you mean. FYI the original use case was crrev.com/2452063003. I originally wanted to end up with a RW array which is pre-populated by the linker (without creating any static initializer) and than I can keep populating afterwards.
Honestly I think at this point I will go back to a plain old c-style struct with no ctors at all.
I did all this for the pure sake of making the struct fields private. My initial line of reasoning was: "I don't trust humans to not touch fields if I don't make them private. I trust the compiler to do what I want here." I did revisit my position, at least on the latter.

I hope at this point I can still rely at least on the fact that initialising an array of structs of PODs (B in this example) doesn't cause static initialisers. Before today I would have put my hand on fire about this. Today maybe a finger at most.
> Or as the style guide says: "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."

Ah, didn't realise about that. That removes all the ambiguity about me trying to do something clever. I wish I did see that few days ago before writing the code.


The paragraph before does say that "However, such variables are allowed if they are constexpr: they have no dynamic initialization or destruction." because it is the dynamic initialization or destruction which is the real concern.

And, to be clear, an array/struct of POD can have a constructor, if it is initialized with a function. Hence this rule: "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"

It all comes down to avoiding constructors and destructors, which you already knew.

I'm not sure that is similar. In the stackoverflow bug we have a static storage duration object that is not initialized at the correct time. In this case we do not have such an object. And, when we tag 'A' as constexpr the problem goes away.

So, I agree with the analysis of the stackoverflow issue that it was a bug (now fixed, as of Update 3) whereas I don't think primiano@'s situation is a bug.
Status: WontFix (was: Untriaged)
Yeah the more I think to this the more I agree that is "something reasonable to expect but not guaranteed to happen".
The fact that std::is_pod == false when adding a constexpr (I found that out only today) is a good hint about that.
Still, I wished that this was properly spec-ed, but right now it seems to not be the case. A constexpr func is not "constexpr-only", and this is not the only case where this applies.

Compilers are smart these days. They have a piece of AI that understands if you are writing something simple, in which case they are friendly and optimize your code, or if you are trying to play smart with them. When the latter happen they take it seriously and punish you showing how undefined behavior looks like. They seem to prefer either doing that on Monday mornings when you are in the process or writing code, or on Friday nights after code has landed and passed CQ. :)
I think MSVC result in this bug can also be explained by the fact that MSVC runs dynamic initializers, and runs them after Initialize() call:

---
In initializer, before changing: a[1].v=0 b[1].v=0        <-- Zero initialization (BSS)
In initializer, after changing:  a[1].v=666 b[1].v=666    <-- Initialize() changes a, b
In main, after changing:         a[1].v=42 b[1].v=666     <-- a[] ctors run after Initialize()
---


brucedawson@, can you run the test on MSVC Update 3?
Cc: dskiba@chromium.org
Test was already run on VS 2015 Update 3 - that's what the original results come from.

Agreed, MSVC is creating a dynamic initializer. This is annoying. MSVC is not required to use a dynamic initializer, but doing so is also not prohibited because the 'A' variable is not tagged as constexpr.
I don't think global variable should be constexpr itself to be guaranteed to be statically initialized. Otherwise std::once_flag wouldn't be possible with just its single requirement of a constexpr ctor.

Also found relevant section on cppreference: http://en.cppreference.com/w/cpp/language/constant_initialization
Hmm the link in #12 is really interesting:
quoting:
-----
Only the following variables are constant initialized:
...
2) Static or thread-local object of class type that is initialized by a constructor call, if the constructor is constexpr and all constructor arguments (including implicit conversions) are constant expressions, and if the initializers in the constructor's initializer list and the brace-or-equal initializers of the class members only contain constant expressions.
...
The effects of constant initialization are the same as the effects of the corresponding initialization, except that it's guaranteed that it is complete before any other initialization of a static or thread-local object begins, and it may be performed at compile time.
----

Now if you look at struct A, that seems to fall in the definition of 2). It's initialized via a constructor call, the ctor is constexpr and all arguments are constexpr (well they are just literals). 
hans: what would your C++ lawyer say about this?
According to this definition the A() ctor that writes 42 is supposed to happen before the non-const Initialize() call.
What am I missing?

P.S. from a practical viewpoint still doesn't make me happy because it says "may" be performed at compile time, which means this might still be a dynamic initializer and hence slow down startup.

Comment 14 by h...@chromium.org, Nov 1 2016

> Now if you look at struct A, that seems to fall in the definition of 2). It's initialized via a constructor call, the ctor is constexpr and all arguments are constexpr (well they are just literals). 
> hans: what would your C++ lawyer say about this?
> According to this definition the A() ctor that writes 42 is supposed to happen before the non-const Initialize() call.
What am I missing?

I didn't consult my lawyer or try the code again, but I suspect there's something subtle going on because 'a' is an array.

As Richard mentioned in that other bug, the text around this, p3.6.2 [basic.start.init], changed in C++14 a little, and this was added:

"A constant initializer for an object o is an expression that is a constant expression, except that it may also invoke constexpr constructors for o and its subobjects even if those objects are of non-literal class types"

And then I suspect 3) might apply, which in C++14 is formulated like:

"if an object with static or thread storage duration is not initialized by a constructor call and if either the object is value-initialized or every full-expression that appears in its initializer is a constant expression"

I wonder if that last "constant expression" is meant to refer to the "constant initializer" definition above.

But the more I learn about this language, the less I understand.

Sign in to add a comment