Ill-formed goto jump in C++ with compile-time known-to-be-false condition: is it actually illegal?

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.


The word “program” refers to the static entity made of code (“process” is the usual word for the dynamic entity, although the standard tends to merely refer to “execution”). Similarly, “ill-formed” is a static property; “undefined behavior” is used to describe “runtime errors”.

if constexpr doesn’t change this analysis simply because no rule says so: if constexpr affects return type deduction (courtesy of [dcl.spec.auto.general]), the necessity of definitions ([basic.def.odr]), and instantiation ([stmt.if] itself), but that’s all. It’s not defined to “omit” one of its branches like #if, which is a common source of confusion when people put something like static_assert(false); or a simple syntax error into one side.

It may be useful to know that C++23 is in the process of changing the quoted sentence to read

Then, all variables that are active at Q but not at P are initialized in declaration order; unless all such variables have vacuous initialization ([basic.life]), the transfer of control shall not be a jump[…].

which is perhaps a bit less easy to read as describing a dynamic prohibition (since the “are initialized in declaration order” is a static description of the behavior, just like the statement that the operand of ++ “is modified”).