函数参数求值和副作用

人气:617 发布:2022-10-16 标签: c++ undefined-behavior c++17 language-lawyer side-effects

问题描述

C++20标准规定函数调用,7.6.1.3/8:

参数的初始化(包括每个关联值计算和副作用)相对于任何其他参数的初始化是不确定的。

不确定排序(相对于非排序)可确保影响相同内存区域的副作用不是未定义的行为。Cp首选项gives the following examples:

f(i = -2, i = -2); // undefined behavior until C++17
f(++i, ++i);       // undefined behavior until C++17, unspecified after C++17

C++17中的更改似乎不在引用的部分中,尽管其措辞在几个世纪几十年中基本上保持不变。(好的;在n3337中,这只是一个注释。)

一个简单的例子引起GCC和clang的警告:

void f(int fa, int fb);

void  m() // contains code calling f()
{
    int a = 11;
    f(++a, ++a);
    cout << "after f(): a=" << a << '
';
}
<source>:6:7: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
    f(++a, ++a);
      ^    ~~

GCC还生成非直观的代码,在将其值移入两个参数寄存器之前,将a递增两次。这与我对标准措辞的理解相矛盾。

堆栈溢出用户Davis Herringmentioned in a comment:初始化已排序,而参数表达式的求值未排序。

我是不是误解了这个措辞?Cpfirst是不是错了?编者错了吗,尤其是GCC?在C++17中,具体的函数参数有什么变化吗?

推荐答案

如果您的问题是参数的初始化是否涉及计算作为其初始化式一部分的表达式...当然是这样的。初始化参数的工作原理与初始化任何其他对象([dcl.init]/1)完全相同:

本子句中描述的初始化过程适用于所有初始化,而与语法上下文无关,包括函数参数的初始化([expr.call])、返回值的初始化([stmt.Return]),或者初始化式位于声明符后。

已添加重点。

完整的[dcl.init]描述了初始化对象的过程,但在所有情况下,它都涉及计算初始化式表达式。因此,这符合排序规则中的所有关联值计算和副作用。

任何不对参数初始值设定项表达式执行此操作的编译器都是错误的。

编译器可以警告任何他们想要的东西。事实上,编译器警告通常是技术上有效但不合理的代码。

更改标准的这一部分的要点是而不是将f(++a, ++a)这样的琐碎胡言乱语变成半合理的代码。它用于处理代码编写者不知道同一对象在多个位置被引用的情况。请考虑:

template<typename ...Args>
void g(Args &&args)
{
  f((++args) ...);
}
在C++17之前,此代码的有效性完全取决于用户是否传递了对同一对象的两个引用,编译器无法合理地警告潜在的问题。在C++17之后,此代码是安全的(模数编译器错误)。

因此,对容易检测到的易混淆代码发出警告不仅是有效的,而且好。

258