I am learning about some dark corners of C++ and, in particular, about the "forbidden" goto
and some restrictions on its usage. This question is partially inspired by Patrice Roy's talk at CppCon 2019 "Some Programming Myths Revisited" (link to exact time with a similar example).
Note, this is a language lawyer question, and I in no way advocate for the usage of goto
in this particular example.
The following C++ code:
#include <iostream>
#include <cstdlib>
struct X {
X() { std::cout<<"X Constructor\n"; }
~X() { std::cout<<"X Destructor\n"; }
};
bool maybe_skip() { return std::rand()%10 != 0; }
int main()
{
if (maybe_skip()) goto here;
X x; // has non-trivial constructor; thus, preventing jumping over itself
here:
return 0;
}
is ill-formed and won't compile. Since goto
can skip the initialization of x
of type X
that has a non-trivial constructor.
Error message from Apple Clang:
error: cannot jump from this goto statement to its label if (maybe_skip()) goto here; ^ note: jump bypasses variable initialization X x; ^
This is clear to me.
However, what is not clear, is why the variations of this with the constexpr
qualifier
constexpr bool maybe_skip() { return false; }
or even simply going with an always false
if-condition known in compile-time
#include <iostream>
struct X {
X() { std::cout<<"X Constructor\n"; }
~X() { std::cout<<"X Destructor\n"; }
};
constexpr bool maybe_skip() { return false; } // actually cannot skip
int main()
{
// if constexpr (maybe_skip()) goto here;
if constexpr (false) goto here;
X x; // has non-trivial constructor; thus, preventing jumping over itself
here:
return 0;
}
is also ill-formed (tried on Apple Clang 11.0.3 and GCC 9.2).
According to Sec. 9.7 of N4713:
It is possible to transfer into a block, but not in a way that bypasses declarations with initialization. A program that jumps 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 (11.6).
So, does my second version of the program with if constexpr (false) goto here;
actually "jump" in the eyes of the compiler, even though at the end of the day it would have deleted this "jump" anyway? (.constexpr
in the last case with plain false
is mostly redundant, but left for consistency)
I might be missing the exact phrasing or interpretation of the standard, or the "order of the operations" because in my [apparently, faulty] logic, the illegal jump does not and cannot happen.