我正在学习 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
是已知的)。在模板实例化期间没有消除的丢弃子语句(因为它根本不在模板内,或者因为条件不依赖)仍然“编译”,除了:
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恰好是int
或 X
.当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/