First of all, the rule about goto
not being allowed to skip over a nontrivial initialization is a compile-time rule. If a program contains such a goto
, the compiler is required to issue a diagnostic.
Now we turn to the question of whether if constexpr
can "delete" the offending goto
statement and thereby erase the violation. The answer is: only under certain conditions. The only situation where the discarded substatement is "truly eliminated" (so to speak) is when the if constexpr
is inside a template and we are instantiating the last template after which the condition is no longer dependent, and at that point the condition is found to be false
(C++17 [stmt.if]/2). In this case the discarded substatement is not instantiated. For example:
template <int x>
struct Foo {
template <int y>
void bar() {
if constexpr (x == 0) {
// (*)
}
if constexpr (x == 0 && y == 0) {
// (**)
}
}
};
Here, (*)
will be eliminated when Foo
is instantiated (giving x
a concrete value). (**)
will be eliminated when bar()
is instantiated (giving y
a concrete value) since at that point, the enclosing class template must have already been instantiated (thus x
is already known).
A discarded substatement that is not eliminated during template instantiation (either because it is not inside a template at all, or because the condition is not dependent) is still "compiled", except that:
- the entities referenced therein are not odr-used (C++17 [basic.def.odr]/4);
- any
return
statements located therein do not participate in return type deduction (C++17 [dcl.spec.auto]/2).
Neither of these two rules will prevent a compilation error in the case of a goto
that skips over a variable with nontrivial initialization. In other words, the only time when a goto
inside a discarded substatement, that skips over a nontrivial initialization, will not cause a compilation error is when the goto
statement "never becomes real" in the first place due to being discarded during the step in template instantiation that would normally create it concretely. Any other goto
statements are not saved by either of the two exceptions above (since the issue is not with odr-use, nor return type deduction).
Thus, when (similarly to your example) we have the following not inside any template:
// Example 1
if constexpr (false) goto here;
X x;
here:;
Therefore, the goto
statement is already concrete, and the program is ill-formed. In Example 2:
// Example 2
template <class T>
void foo() {
if constexpr (false) goto here;
X x;
here:;
}
if foo<T>
were to be instantiated (with any argument for T
), then the goto
statement would be instantiated (resulting in a compilation error). The if constexpr
would not protect it from instantiation, because the condition doesn't depend on any template parameters. In fact, in example 2, even if foo
is never instantiated, the program is ill-formed NDR (i.e., the compiler may be able to figure out that it will always cause an error regardless of what T
is, and thus diagnose this even before instantiation) (C++17 [temp.res]/8.
Now let's consider example 3:
// Example 3
template <class T>
void foo() {
if constexpr (false) goto here;
T t;
here:;
}
the program will be well-formed if, say, we only instantiate foo<int>
. When foo<int>
is instantiated, the variable skipped over has trivial initialization and destruction, and there is no problem. However, if foo<X>
were to be instantiated, then an error would occur at that point: the whole body including the goto
statement (which skips over the initialization of an X
) would be instantiated at that point. Because the condition is not dependent, the goto
statement is not protected from instantiation; one goto
statement is created every time a specialization of foo
is instantiated.
Let's consider example 4 with a dependent condition:
// Example 4
template <int n>
void foo() {
if constexpr (n == 0) goto here;
X x;
here:;
}
Prior to instantiation, the program contains a goto
statement only in the syntactic sense; semantic rules such as [stmt.dcl]/3 (the prohibition on skipping over an initialization) are not applied yet. And, in fact, if we only instantiate foo<1>
, then the goto
statement is still not instantiated and [stmt.dcl]/3 is still not triggered. However, regardless of whether the goto
is ever instantiated at all, it remains true that if it were to be instantiated, it would always be ill-formed. [temp.res]/8 says the program is ill-formed NDR if the goto
statement is never instantiated (either because foo
itself is never instantiated, or the specialization foo<0>
is never instantiated). If instantiation of foo<0>
occurs, then it's just ill-formed (diagnostic is required).
Finally:
// Example 5
template <class T>
void foo() {
if constexpr (std::is_trivially_default_constructible_v<T> &&
std::is_trivially_destructible_v<T>) goto here;
T t;
here:;
}
Example 5 is well-formed regardless of whether T
happens to be int
or X
. When foo<X>
is instantiated, because the condition depends on T
, [stmt.if]/2 kicks in. When the body of foo<X>
is being instantiated, the goto
statement is not instantiated; it exists only in a syntactic sense and [stmt.dcl]/3 is not violated because there is no goto
statement. As soon as the initialization statement "X t;
" is instantiated, the goto
statement disappears at the same time, so there is no problem. And of course, if foo<int>
is instantiated, whereupon the goto
statement is instantiated, it only skips over the initialization of an int
, and there is no problem.