除了未定义、未指定和实现定义的行为之外,C++26 还引入了错误行为(请参阅)。这种新的构造与现有的行为有何不同?为什么将其添加到 C++ 标准中?

7

  • 9
    噢,天哪,就在您认为自己已经掌握了 C++ 的窍门的时候。


    – 

  • 1
    更有趣的问题是,它与病态有何不同?由于格式不正确的程序也“需要”产生诊断。基本上,只要编译器产生诊断(以任何形式,如警告等),就允许编译器编译格式错误的程序。


    – 


  • 2
    似乎标准中又添加了另一个麻烦。避免学习 C++ 的又一个原因。


    – 


  • @john:永远不要觉得有义务遵循最新标准。标准委员会的动机不一定与您自己的动机一致。


    – 

  • 1
    @user12002570,格式错误不是一种行为。 “未定义”、“未指定”和“依赖于实现”是 C++ 程序在运行时会做什么的不同程度的不确定性,但如果它的格式不正确,那么它甚至不是 C++ 程序。 (另请注意,有两种不同程度的“格式错误”。有普通的“格式错误”,编译器需要拒绝它,然后有“格式错误,无需诊断”,这是格式错误的,但以某种微妙的方式,您不会期望编译器能够轻松检测到。en.cppreference.com/w/cpp/language/


    – 



1 个回答
1

错误行为是“有缺陷的”或“不正确的”行为,如所解释的那样。该提案将错误行为引入了 C++26,将之前未定义的行为转变为错误行为。

最显着的区别是,未定义的行为对于程序可以执行的操作没有限制,包括跳转到“随机”函数、访问不应访问的内存以及其他不利于安全的影响。错误行为的形式是([defns.erroneous]):

建议实施来诊断的明确定义的行为

可以通过警告、运行时错误等来诊断;正式地,[intro.abstract] p4.1.2 解释说:

如果执行包含指定为具有错误行为的操作,则允许该实现发出诊断并允许在该操作之后的未指定时间终止执行。

动机

不幸的是,大量的 C++ 代码并非没有错误,并且许多错误可能会危害安全。一个明显的例子是这样的:

void (*f)(); // uninitialized function pointer;
             // basically an abstraction for an instruction address
// ...
f();         // what address do we jump to?

如果f占用堆栈上的一些空间,攻击者可以确保在执行此代码之前堆栈上的内存具有他们选择的值。
f()因此,攻击者可以跳转到他们想要的程序中的任何指令。的此类案例还有很多

简单地通过默认初始化函数指针来使这段代码“正确”nullptr也是没有意义的,因为这里显然存在一个错误。
f应该已经初始化,如果我们在调用之前忘记初始化它,将其存储在某个地方等,编译器应该让我们注意这一事实。f我们不希望这个错误只是“被掩盖”。

错误的行为是如何发生的?

错误的行为始于错误的值,例如,当变量未初始化时会产生错误的值。附带说明一下,可以使用以下属性重现 C++26 之前的行为[[indeterminate]]

void f(int);

int indet [[indeterminate]]; // indet has indeterminate value
int erron;                   // erron has erroneous value ([basic.indet])

f(indet); // undefined behavior
f(erron); // erroneous behavior

如上所述,未定义的行为可以在这里执行任何操作,包括跳转到 以外的函数f,而f(erron)应该始终具有已定义的行为,但应该在某个时刻进行诊断。

错误与格式错误

程序类似,因为两者都应该导致诊断(另请参见)。

然而,错误行为在程序执行期间生效,而程序在翻译(编译)期间格式错误。例如:

int x = float; // ill-formed; not valid C++ code,
               // shall be diagnosed

int y;         // well-formed (valid C++ code) but y has erroneous value
int z = y;     // erroneous behavior, should be diagnosed

常量表达式中的错误行为

与未定义的行为不同,错误的行为总是使表达式失去常量表达式 ([expr.const]) 的资格。请注意,未定义的行为在大多数情况下表现相同,但例如大多数标准库中失败的Precondition[[assume]]或失败的属性仍然可能导致常量表达式内部出现 UB。

此外,constexpr对象不能有错误的值:

constexpr int x; // error: x has erroneous value

在 C++23 中,这也是格式错误的,因为x它具有不确定的值。

更广阔的前景

总的来说,C++ 开发人员和 C++ 委员会正在推动该语言向“更安全”的方向发展。作为其中的一部分,未来几年大量未定义的行为可能会变成错误的行为。

在某些情况下,已经有一个非常积极的提案,例如。其他一些未定义行为的情况(例如有符号整数溢出、除以零等)可能会出错。

难以诊断的 UB 形式,例如数据争用或无效的向下转换(使用static_cast)可能会保持未定义状态,甚至可能无限期地保持不变。

错误行为的代价

编译器越来越依赖未定义的行为来达到优化的目的。例如:

void f(int i) {
    int arr[1] { 123 };
    return arr[i];
}

编译器可以将其优化为:

void f(int):
    mov eax, 123
    ret

如果i是 以外的任何值0,则该数组arr将被越界访问,这是未定义的行为。编译器可以假设 UB 根本不会发生并进行相应的优化。如果越界访问数组变成了错误行为,则鼓励编译器向数组访问添加运行时边界检查,如果i不是则终止程序0

总之,错误行为不是“自由的”;而是“自由的”。它是以性能为代价的。错误行为通常被添加到 C++ 标准中,其中未定义行为的安全风险很大,并且未定义行为的原因通常不用于优化。

14

  • “建议实施诊断的明确定义的行为..”因此与格式不正确相同,只是标准只是将“必需”一词更改为“推荐”。


    – 

  • 1
    @Alan我添加了一个部分来讨论格式不正确的程序和错误行为之间的区别。


    – 

  • 我懂了。但为什么在没有使用 odr 的情况下会出现int y;错误呢? “错误值”也与“不确定值”相同y


    – 


  • 1
    @Alan它有错误的值,这还不是错误的行为,但是例如这样做int z = y;将是错误的行为。我已经更新了示例来展示这一点。错误值与不确定值不同;在大多数情况下使用不确定的值会导致未定义的行为,而使用错误的值会导致错误的行为。


    – 


  • 1
    “鼓励编译器对数组访问添加运行时边界检查,如果 i 不为 0,则终止程序。” – 您在论文中的哪个位置看到错误行为应导致终止(或边界检查)的建议?


    –