Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
244 views
in Technique[技术] by (71.8m points)

language lawyer - 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
"; }
    ~X() { std::cout<<"X Destructor
"; }
};

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
"; }
    ~X() { std::cout<<"X Destructor
"; }
};

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.

question from:https://stackoverflow.com/questions/64851830/ill-formed-goto-jump-in-c-with-compile-time-known-to-be-false-condition-is-it

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...