函数参数评估和副作用

Pet*_*ica 18 c++ side-effects undefined-behavior language-lawyer c++17

C++20 标准在Function Call 7.6.1.3/8 中说:

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

不确定排序(与排序相反)可确保影响同一内存区域的副作用不是未定义的行为。Cppreference给出了以下示例

f(i = -2, i = -2); // C++17 之前未定义的行为
f(++i, ++i); // C++17 之前未定义的行为, 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 << '\n';
}
Run Code Online (Sandbox Code Playgroud)
<source>:6:7: warning: multiple unsequenced modifications to 'a' [-Wunsequenced]
    f(++a, ++a);
      ^    ~~
Run Code Online (Sandbox Code Playgroud)

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

stackoverflow 用户 Davis Herring在评论中提到,虽然初始化是按顺序进行的,但参数表达式的求值却不是。

我是否误解了这个措辞?cppreference错误吗?编译器是不是错了,尤其是gcc?C++17 中关于函数参数的具体变化是什么(如果有的话)?

Nic*_*las 6

如果您的问题是“参数的初始化”是否涉及评估作为其初始值设定项一部分的表达式……当然是这样。初始化参数的工作方式与初始化任何其他对象 ( [dcl.init]/1 )完全相同:

本小节中描述的初始化过程适用于所有初始化,无论语法上下文如何,包括函数参数的初始化([expr.call])、返回值的初始化([stmt.return])或初始化程序时跟随一个声明符。

添加了强调。

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

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

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

即使改变这部分标准的要点也不是把琐碎的废话f(++a, ++a)变成半合理的代码。它用于处理代码编写者不知道同一对象在多个位置被引用的情况。考虑:

template<typename ...Args>
void g(Args &&args)
{
  f((++args) ...);
}
Run Code Online (Sandbox Code Playgroud)

在 C++17 之前,此代码的有效性完全取决于用户是否传递了对同一对象的两个引用,并且编译器无法合理地警告潜在问题。在 C++17 之后,此代码是安全的(模编译器错误)。

因此,对容易检测到的令人困惑的代码发出警告不仅是有效的,而且是好的

  • @Peter-ReinstateMonica:成为一名语言律师和拥有深奥的规则之间是有区别的。列表初始化是一个深奥的规则,有很多陷阱。参数的初始化是否包括对正在执行初始化的表达式进行求值,以便确定这些求值与函数调用中其他求值的顺序是否相同,以便人们可以询问是否发生了永远不应该编写的*无意义代码*是合法的吗?*那是*语言律师。 (2认同)