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
可以跳过具有非平凡构造函数的类型 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 上试过)。
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
的初始化,并且没问题。
我正在学习 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 上试过)。
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
的初始化,并且没问题。