C++ 中格式错误的 goto 跳转具有编译时已知为假的条件:它实际上是非法的吗?

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

我正在学习 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 可以跳过具有非平凡构造函数的类型 Xx 的初始化。

来自 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 的情况下大部分是多余的,但为了保持一致性而保留).

我可能遗漏了标准的确切措辞或解释,或“操作顺序”,因为在我的[显然是错误的]逻辑中,非法跳转不会也不会发生。

“程序”一词是指由代码构成的静态实体(“过程”是动态实体的常用词,尽管标准倾向于仅指“执行”)。同样,“病态”是静态的属性; “未定义行为”用于描述“运行时错误”。

if constexpr 不会仅仅因为没有规则这么说而改变这个分析:if constexpr 影响 return 类型推导(由 [dcl.spec.auto.general] 提供),必要性定义([basic.def.odr])和实例化([stmt.if] 本身),仅此而已。它没有被定义为 “省略” 它的一个分支,例如 #if,当人们输入 static_assert(false); 或简单的语法错误时,这是​​一个常见的混淆来源进入一侧。

了解 C++23 正在将引用的句子更改为 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[…].

这可能不太容易理解为描述动态禁止(因为“按声明顺序初始化”是对行为的静态描述,就像 ++ 的操作数的语句“被修改”)。

首先,关于 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 一个具体值),因为此时封闭的 class 模板必须已经被实例化(因此 x 已经知道了)。

在模板实例化过程中没有消除的废弃子语句(因为它根本不在模板内,或者因为条件不依赖)仍然是“编译的”,除了:

  • 其中引用的实体未被 ODR 使用 (C++17 [basic.def.odr]/4);
  • 位于其中的任何 return 语句不参与 return 类型推导 (C++17 [dcl.spec.auto]/2).

goto 跳过具有非平凡初始化的变量的情况下,这两条规则都不能防止编译错误。换句话说,只有当 goto 在丢弃的子语句中跳过重要的初始化时,才会 而不是 导致编译错误是 goto语句“永远不会变成真实的”首先是因为在模板实例化的步骤中被丢弃,通常会具体地创建它。任何其他 goto 语句都不会被上述两个异常中的任何一个保存(因为问题不在于 odr-use,也不在于 return 类型推导)。

因此,当(类似于您的示例)我们在任何模板中都没有以下内容时:

// 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.res]/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 语句不受实例化保护;每次实例化 foo 的特化时都会创建一个 goto 语句。

让我们考虑具有依赖条件的示例 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 语句从未被实例化(或者因为 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 恰好是 int 还是 X,示例 5 都是合式的。当 foo<X> 被实例化时,因为条件取决于 T,[stmt.if]/2 开始。当 foo<X> 的主体被实例化时, goto 语句 实例化;它仅存在于句法意义上并且 [stmt.dcl]/3 没有被违反,因为 没有 goto 语句 。初始化语句“X t;”一实例化,goto语句同时消失,所以没有问题。当然,如果foo<int>被实例化,goto语句实例化,它只跳过int的初始化,并且没问题。