c++ - C++ 中格式错误的 goto 跳转,编译时已知为假条件 : is it actually illegal?

标签 c++ language-lawyer goto

我正在学习 C++ 的一些黑暗角落,特别是关于“被禁止的”goto以及对其使用的一些限制。这个问题的部分灵感来自 Patrice Roy's talk at CppCon 2019 "Some Programming Myths Revisited" ( link to exact time with a similar example )。
注意,这是一个语言律师的问题,我绝不提倡使用 goto在这个特定的例子中。

以下 C++ 代码:

#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;
}
格式错误,无法编译。自 goto可以跳过x的初始化类型 X它有一个非平凡的构造函数。
来自 Apple Clang 的错误消息:
error: cannot jump from this goto statement to its label
if (maybe_skip()) goto here;
                  ^
note: jump bypasses variable initialization
X x;
  ^

这对我来说很清楚。
然而,不清楚的是,为什么 constexpr 会出现这种变化。预选赛
constexpr bool maybe_skip() { return false; }
甚至只是简单地使用 false编译时已知的 if 条件
#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;
}
也是格式错误的(在 Apple Clang 11.0.3 和 GCC 9.2 上尝试过)。
根据 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).


所以,我的第二个版本程序的 if constexpr (false) goto here; 实际上在编译器眼中“跳跃” ,即使在一天结束时它也会删除这个“跳转”? ( constexpr 在最后一种情况下,纯 false 大部分是多余的,但为了一致性而保留)。
我可能遗漏了标准的确切措辞或解释,或“操作顺序”,因为在我的 [显然是错误的] 逻辑中,非法跳转不会也不会发生。

最佳答案

首先,关于goto的规则不允许跳过重要的初始化是编译时规则。如果一个程序包含这样一个 goto ,编译器需要发出诊断信息。
现在我们转向是否if constexpr的问题可以“删除”违规goto声明,从而消除违规行为。答案是:只有在特定条件下。丢弃的子语句“真正消除”(可以这么说)的唯一情况是 if constexpr在模板内,我们正在实例化最后一个模板,之后条件不再依赖,此时条件被发现为 false (C++17 [stmt.if]/2)。在这种情况下,丢弃的子语句不会被实例化。例如:

template <int x>
struct Foo {
    template <int y>
    void bar() {
        if constexpr (x == 0) {
            // (*)
        }
        if constexpr (x == 0 && y == 0) {
            // (**)
        }
    }
};
在这里,(*) Foo时会被淘汰被实例化(给 x 一个具体的值)。 (**) bar()时会被淘汰被实例化(给 y 一个具体的值),因为在这一点上,封闭的类模板必须已经被实例化(因此 x 是已知的)。
在模板实例化期间没有消除的丢弃子语句(因为它根本不在模板内,或者因为条件不依赖)仍然“编译”,除了:
  • 其中引用的实体未使用 odr (C++17 [basic.def.odr]/4);
  • 任何 return位于其中的语句不参与返回类型推导 (C++17 [dcl.spec.auto]/2)。

  • goto 的情况下,这两条规则都不会阻止编译错误。跳过具有非平凡初始化的变量。换句话说,只有一次 goto在丢弃的子语句中,跳过非平凡的初始化,当 goto 时不会导致编译错误声明“永远不会成为现实”首先是因为在模板实例化的步骤中被丢弃,通常会具体地创建它。任何其他 goto上述两个异常(exception)中的任何一个都不会保存语句(因为问题不在于 odr 使用,也不在于返回类型推导)。
    因此,当(类似于您的示例)我们在任何模板中都没有以下内容时:
    // Example 1
    if constexpr (false) goto here;
    X x;
    here:;
    
    因此,goto语句已经是具体的,程序格式错误。在示例 2 中:
    // Example 2
    template <class T>
    void foo() {
        if constexpr (false) goto here;
        X x;
        here:;
    }
    
    如果 foo<T>将被实例化(使用 T 的任何参数),然后是 goto语句将被实例化(导致编译错误)。 if constexpr不会保护它免于实例化,因为条件不依赖于任何模板参数。实际上,在示例 2 中,即使 foo永远不会被实例化 ,程序是格式错误的 NDR(即,无论 T 是什么,编译器都可能会发现它总是会导致错误,因此甚至在实例化之前就可以诊断出来)(C++17 [temp.资源]/8。
    现在让我们考虑示例 3:
    // Example 3
    template <class T>
    void foo() {
        if constexpr (false) goto here;
        T t;
        here:;
    }
    
    如果我们只实例化 foo<int>,程序将是良构的。 .当foo<int>被实例化了,跳过的变量初始化和销毁​​都是微不足道的,没有问题。但是,如果 foo<X>被实例化,那么此时会发生错误:包括 goto 在内的整个主体语句(跳过 X 的初始化)将在那时被实例化。因为条件不依赖,goto语句不受实例化保护;一 goto每次对 foo 进行特化时都会创建语句被实例化。
    让我们考虑具有依赖条件的示例 4:
    // Example 4
    template <int n>
    void foo() {
        if constexpr (n == 0) goto here;
        X x;
        here:;
    }
    
    在实例化之前,程序包含一个 goto仅在句法意义上的陈述;语义规则如 [stmt.dcl]/3(禁止跳过初始化)尚未应用。而且,事实上,如果我们只实例化 foo<1> ,然后是 goto语句仍未实例化并且 [stmt.dcl]/3 仍未触发。但是,不管是否goto从来没有被实例化过,如果它被实例化,它总是格式错误的。 [temp.res]/8 表示如果 goto 程序是格式错误的 NDR语句永远不会被实例化(因为 foo 本身永远不会被实例化,或者特化 foo<0> 永远不会被实例化)。如果实例化 foo<0>发生,那么它只是格式错误(需要诊断)。
    最后:
    // 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:;
    }
    
    无论 T 是否为良构示例 5恰好是intX .当foo<X>被实例化,因为条件依赖于 T , [stmt.if]/2 开始。当 foo<X> 的正文时正在实例化,goto语句未实例化;它只存在于句法意义上并且 [stmt.dcl]/3 没有被违反,因为没有 goto陈述。一旦初始化语句“X t;”被实例化,goto语句同时消失,所以没有问题。当然,如果 foo<int>被实例化,于是 goto语句被实例化,它只会跳过 int 的初始化,并且没有问题。

    关于c++ - C++ 中格式错误的 goto 跳转,编译时已知为假条件 : is it actually illegal?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64851830/

    相关文章:

    c++ - C++ 中 "array of unknown bound of T"的外部声明

    c++ - 放置 `new` 可以依赖底层存储值吗?

    c++ - 错误 C2059 : syntax error: '}' C++

    c++ - 如何退出for循环内嵌套的if-else语句?(C/C++)

    c++ - 数组作为参数

    c++ - vector 中的指针问题

    c++ - 如何从终端分离以便 git 中的 post-receive hook 完成

    c++ - 并发读取非原子变量

    c++ - R Makevars 文件没有正确覆盖 g++

    c++ - 使用 "for"打破 "break"循环被认为是有害的?