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

Ant*_*hov 41 c++ goto language-lawyer

我正在学习 C++ 的一些黑暗角落,特别是关于“禁止”goto及其使用的一些限制。这个问题的部分灵感来自Patrice Roy 在 CppCon 2019 “Some Programming Myths Revisited”上的演讲链接到具有类似示例的确切时间)。

请注意,这是一个语言律师问题,我绝不提倡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;
}
Run Code Online (Sandbox Code Playgroud)

格式错误,无法编译。因为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;
  ^
Run Code Online (Sandbox Code Playgroud)

这对我来说很清楚。

然而,不清楚的是,为什么 this 与constexpr限定符的变化

constexpr bool maybe_skip() { return false; }
Run Code Online (Sandbox Code Playgroud)

甚至只是简单地使用false编译时已知的始终if 条件

error: cannot jump from this goto statement to its label
if (maybe_skip()) goto here;
                  ^
note: jump bypasses variable initialization
X x;
  ^
Run Code Online (Sandbox Code Playgroud)

也是格式错误的(在 Apple Clang 11.0.3 和 GCC 9.2 上尝试过)。

根据秒。N4713 的 9.7

可以转移到块中,但不能通过初始化绕过声明。一个程序从具有自动存储期的变量不在范围内的点跳转到它在范围内的点是格式错误的,除非变量具有标量类型、具有平凡默认构造函数和平凡析构函数的类类型,这些类型之一的 cv 限定版本,或前述类型之一的数组,并且在没有初始化程序的情况下声明 (11.6)。

那么,的程序的第二个版本if constexpr (false) goto here; 在编译器眼中是否真的“跳转”了,即使在一天结束时它无论如何都会删除这个“跳转”?constexpr在最后一种情况下,plainfalse大多是多余的,但为了一致性而保留)

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

Bri*_*ian 34

首先,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) {
            // (**)
        }
    }
};
Run Code Online (Sandbox Code Playgroud)

在这里,(*)将在Foo实例化时消除(给出x具体值)。(**)将在bar()实例化(给出y具体值)时消除,因为在那时,封闭类模板必须已经实例化(因此x是已知的)。

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

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

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

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

// Example 1
if constexpr (false) goto here;
X x;
here:;
Run Code Online (Sandbox Code Playgroud)

因此,goto语句已经是具体的,程序是格式错误的。在示例 2 中:

// Example 2
template <class T>
void foo() {
    if constexpr (false) goto here;
    X x;
    here:;
}
Run Code Online (Sandbox Code Playgroud)

如果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:;
}
Run Code Online (Sandbox Code Playgroud)

例如,如果我们只实例化 ,程序将是良构的foo<int>。当foo<int>被实例化,变量跳过有琐碎的初始化和销毁,并没有问题。但是,如果foo<X>要实例化,那么此时会发生错误:包括goto语句(跳过 an 的初始化X)在内的整个主体都将在此时实例化。因为条件不依赖,所以goto语句不受实例化保护;goto每次foo实例化 的特化时都会创建一个语句。

让我们考虑具有依赖条件的示例 4:

// Example 4
template <int n>
void foo() {
    if constexpr (n == 0) goto here;
    X x;
    here:;
}
Run Code Online (Sandbox Code Playgroud)

在实例化之前,程序goto只包含句法意义上的语句;语义规则如 [stmt.dcl]/3(禁止跳过初始化)尚未应用。而且,事实上,如果我们只实例化foo<1>,那么goto语句仍然没有实例化并且 [stmt.dcl]/3 仍然没有触发。然而,无论 是否goto曾经被实例化,如果它被实例化,它总是格式错误的。[temp.res]/8 表示如果goto语句从未被实例化(因为foo它本身从未被实例化,或者特foo<0>化从未被实例化),则该程序是格式错误的 NDR 。如果实例化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:;
}
Run Code Online (Sandbox Code Playgroud)

例 5 是良构的,无论Tint还是X。whenfoo<X>被实例化,因为条件依赖于T,[stmt.if]/2 开始。当foo<X>实例化的主体时,goto语句没有被实例化;它只存在于句法意义上,并且 [stmt.dcl]/3 没有被违反,因为没有goto声明。初始化语句“ X t;”一实例化,goto语句同时消失,所以没有问题。当然,如果foo<int>被实例化,那么goto语句实例化,它只会跳过一个int,并且没有问题。