Reputation: 113
My understanding is that a function scope static constexpr
is evaluated at compile time. If this is the case, what justification, if any, does MSVC have for the following error:
int main()
{
int i = 5;
switch (i)
{
// Original question:
static constexpr int j = 7; // legal?
// My actual use case, which may not be legal.
static constexpr int k[2] = { 7, 4 };
default:
case 0:
break;
}
return 0;
}
testapp.cpp(10) : error C2360 : initialization of 'j' is skipped by 'case' label
If that were a non-constexpr then yes, this is a valid complaint. However since the constexpr is evaluated at compile time, there should be no need to execute anything at the declaration site.
-- Edit --
With apologies to Martin Bonner whose answer was deleted since it did not apply to my original question.
My actual use case is the second: a constexpr array with an initializer list. From what I saw in the standard that was cited, my first case of a static constexpr scalar int is not prohibited. However it appears that what I'm trying to do is ill-formed.
If this is indeed true, then why? Isn't the whole point of constexpr to evaluate things at compile time, therefore it should not matter if control ever actually passes the declaration.
Upvotes: 2
Views: 746
Reputation: 41092
It's a MSVC bug.
It's illegal to jump into the scope of a variable declaration+initializer (e.g. case 0: int j = 5;
), but the rules also say that a static
is initialized the first time control passes through their declaration. And declarations, however, are allowed to exist within a switch statement body, according to §6.4.2/6 [stmt.switch]
"...Declarations can appear in the substatement of a switch-statement."
So static
is what makes everything okay in this instance.
You could be totally fine if you had a non-static declaration (without initialization) at the beginning of a switch statement; it's the skipping over of declaration with initialization that makes the compiler bug out.
switch (i)
{
int j;
case 0:
j = 2;
std::cout << 0 << std::endl;
break;
default:
j = 3;
std::cout << j << std::endl;
break;
}
even this is fine (although dangerous):
case 0:
int j;
j = 5;
break;
default:
/*..*/
Because of the rules surrounding control transfer: §6.7/3 [stmt.dcl]:
It is possible to transfer into a block, but not in a way that bypasses declarations with initialization.
I think cppreference explains it better:
If transfer of control enters the scope of any automatic variables (e.g. by jumping forward over a declaration statement), the program is ill-formed (cannot be compiled), unless all variables whose scope is entered have
1) scalar types declared without initializers
...
Upvotes: 2
Reputation:
As @PasserBy hinted in a comment, constexpr
doesn't affect as much as you'd hope. After static constexpr int a = 10;
, code that reads a
's value can be optimised at compile time to use 10
directly, and the compiler will ensure that the initialiser 10
is a compile-time constant, but there is still an object there that in some cases might need actual storage at run-time, including initialisation. For instance when its address is taken.
Initialisation of block-scope static
s may happen early, the same way as for file scope objects, or it may happen when the execution reaches the declaration:
N4140 [dcl.stmt]p4:
[...] An implementation is permitted to perform early initialization of other block-scope variables with static or thread storage duration under the same conditions that an implementation is permitted to statically initialize a variable with static or thread storage duration in namespace scope (3.6.2). Otherwise such a variable is initialized the first time control passes through its declaration; such a variable is considered initialized upon the completion of its initialization. [...]
Now, @MartinBonner's deleted answer quoted [dcl.stmt]p3:
It is possible to transfer into a block, but not in a way that bypasses declarations with initialization. A program that jumps90 from a point where a variable with automatic storage duration is not in scope to a point where it is in scope is ill-formed unless the variable has scalar type, class type with a trivial default constructor and a trivial destructor, a cv-qualified version of one of these types, or an array of one of the preceding types and is declared without an initializer (8.5).
The second sentence only addresses objects with automatic storage duration, but the first doesn't. You're transferring into a block in a way that does potentially bypass a declaration with initialisation. The only thing is, it doesn't necessary bypass a declaration with initialisation: if the initialisation is performed early, no initialisation gets bypassed.
In this particular case, I would hope that all sensible implementations perform early initialisation, but it's not required, and because of that I think the error message is allowed. But I do suspect this isn't MSVC's intended behaviour:
Here's an example where you'd definitely want an error:
int &f() { static int i; return i; }
int main() {
switch (0) {
static int &i = f();
case 0:
return i;
}
}
Here, the initialisation of the variable i
is skipped, and as a result, an uninitialised reference is accessed, triggering a segmentation fault on multiple compilers. Yet in this case, MSVC does not show a compilation error. (Nor do other compilers.) It doesn't make sense for the error message to be issued in a situation where the error is harmless, but omitted where the error is harmful. Because of that, I suspect the error message you're receiving is not intended, and worth reporting as a bug.
Upvotes: 4