Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 778
Anton Menshov Avatar asked Nov 16 '20 02:11

Anton Menshov


People also ask

Is goto a conditional statement?

The goto statement is often combined with the if statement to cause a conditional transfer of control. Programming languages impose different restrictions with respect to the destination of a goto statement.

Why goto statement is avoided?

NOTE − Use of goto statement is highly discouraged in any programming language because it makes difficult to trace the control flow of a program, making the program hard to understand and hard to modify. Any program that uses a goto can be rewritten to avoid them.

When goto statement is used in a program?

The goto statement can be used to alter the flow of control in a program. Although the goto statement can be used to create loops with finite repetition times, use of other loop structures such as for, while, and do while is recommended. The use of the goto statement requires a label to be defined in the program.

When should use goto?

When Should We Use the goto Statement? Goto statement is used for flow control in programs. As discussed above, it transfers the code execution of code from one part of the program to another. Hence, in any case, where you need to make a jump from one part of the code to another, you can use the goto statement.


1 Answers

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.

like image 104
Brian Bi Avatar answered Sep 18 '22 13:09

Brian Bi