C++17 引入的评估顺序保证是什么

What are the evaluation order guarantees introduced by C++17?

本文关键字:是什么 顺序 评估 C++17      更新时间:2023-10-16

C++17 评估顺序保证 (P0145( 中投票对典型C++代码有什么影响?

下面这样的事情会有什么变化?

i = 1;
f(i++, i)

std::cout << f() << f() << f();

f(g(), h(), j());

到目前为止尚未指定求值顺序的一些常见情况已指定并有效 C++17 .一些未定义的行为现在改为未指定。

i = 1;
f(i++, i)

未定义,但现在未指定。具体来说,未指定的是相对于其他参数计算要f的每个参数的顺序。 i++可能会在i之前进行评估,反之亦然。实际上,它可能会以不同的顺序评估第二个调用,尽管它们位于同一个编译器下。

但是,在执行任何其他参数之前,需要对每个参数进行评估,并带有所有副作用。因此,您可能会得到f(1, 1)(首先计算第二个参数(或f(1, 2)(第一个参数首先计算(。但是你永远不会得到f(2, 2)或其他任何类似性质的东西。

std::cout << f() << f() << f();

未指定,但它将与运算符优先级兼容,以便f的第一次评估将在流中排在第一位(下面的示例(。

f(g(), h(), j());

仍然有未指定的 g、h 和 j 求值顺序。请注意,对于getf()(g(),h(),j()),规则规定getf()将在g, h, j之前被求值。

另请注意提案文本中的以下示例:

 std::string s = "but I have heard it works even if you don't believe in it"
 s.replace(0, 4, "").replace(s.find("even"), 4, "only")
  .replace(s.find(" don't"), 6, "");

该示例来自 The C++ Programming Language, 4th Edition, Stroustrup,曾经是未指定的行为,但在 C++17 中,它将按预期工作。可恢复函数(.then( . . . )(也存在类似的问题。

作为另一个示例,请考虑以下内容:

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
struct Speaker{
    int i =0;
    Speaker(std::vector<std::string> words) :words(words) {}
    std::vector<std::string> words;
    std::string operator()(){
        assert(words.size()>0);
        if(i==words.size()) i=0;
        // Pre-C++17 version:
        auto word = words[i] + (i+1==words.size()?"n":",");
        ++i;
        return word;
        // Still not possible with C++17:
        // return words[i++] + (i==words.size()?"n":",");
    }
};
int main() {
    auto spk = Speaker{{"All", "Work", "and", "no", "play"}};
    std::cout << spk() << spk() << spk() << spk() << spk() ;
}

对于 C++14 和之前,我们可能会(并且将(获得以下结果:

play
no,and,Work,All,

而不是

All,work,and,no,play

请注意,上述内容实际上与

(((((std::cout << spk()) << spk()) << spk()) << spk()) << spk()) ;

但是,在 C++17 之前,不能保证第一个呼叫会首先进入流。

参考资料:从接受的提案中:

后缀表达式从左到右计算。这包括函数调用和成员选择表达式。

赋值表达式从右到左计算。这包括复合分配。

移位运算符的操作数从左到右计算。在总结,以下表达式按 a 顺序计算,然后b,然后是c,然后d:

  1. A.B
  2. A->B
  3. A->*B
  4. A(B1, B2, B3(
  5. b @= a
  6. a[b]
  7. a <<b
  8. a>> b

此外,我们建议以下附加规则:顺序涉及重载运算符的表达式的计算为由与相应内置相关联的顺序决定运算符,而不是函数调用的规则。

编辑注释:我的原始答案误解了a(b1, b2, b3)b1b2b3的顺序仍未指定。(谢谢@KABoissonneault,所有评论者。

然而,(正如@Yakk所指出的(这很重要:即使b1b2b3是非平凡的表达式,在开始计算其他表达式之前,它们中的每一个都被完全计算并绑定到相应的函数参数。该标准是这样规定的:

§5.2.2 - 函数调用 5.2.2.4:

。后缀表达式在表达式列表和任何默认参数。每个值计算和与参数初始化相关的副作用,以及初始化本身,在每个值计算之前进行排序,并且与任何后续初始化相关的副作用参数。

但是,GitHub 草案中缺少以下新句子之一:

与参数的初始化和初始化本身是在每个值计算和相关副作用之前排序与任何后续参数的初始化。

例子在那里。它解决了几十年前的问题(正如赫伯·萨特所解释的那样(,除了安全性之外,比如

f(std::unique_ptr<A> a, std::unique_ptr<B> b);
f(get_raw_a(), get_raw_a());

如果其中一个调用get_raw_a()在另一个调用之前抛出,则会泄漏原始指针与其智能指针参数相关联。

正如 T.C. 所指出的,该示例存在缺陷unique_ptr因为从原始指针构造是显式的,从而阻止了编译。

还要注意这个经典问题(标记为 C,而不是 C++(:

int x=0;
x++ + ++x;

仍未定义。

在 C++17 中禁止交错

在C++14中,以下内容不安全:

void foo(std::unique_ptr<A>, std::unique_ptr<B>);
foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));

在函数调用期间,此处发生了四个操作

  1. new A
  2. unique_ptr<A>构造函数
  3. new B
  4. unique_ptr<B>构造函数

这些顺序是完全没有指定的,因此完全有效的排序是(1(,(3(,(2(,(4(。如果选择了此顺序并且 (3( 抛出,则来自 (1( 的内存泄漏 - 我们还没有运行 (2(,这可以防止泄漏。

<小时 />

在C++17中,新规则禁止交错。来自 [介绍执行]:

对于

每个函数调用 F,对于在 F 中发生的每个评估 A,以及每个未在 F 中发生但在同一线程上作为同一信号处理程序的一部分(如果有(进行评估的评估

B,要么在 B 之前排序 A,要么在 B 之前排序 B,要么在 A 之前对 B 进行排序。

这句话有一个脚注,内容如下:

换句话说,函数执行不会相互交错。

这给我们留下了两个有效的排序:(1(、(2(、(3(、(4( 或 (3(、(4(、(1(、(2(。没有指定采用哪种顺序,但这两种顺序都是安全的。现在禁止所有 (1( (3( 都发生在 (2( 和 (4( 之前的订购。

我找到了一些关于表达式求值顺序的注释:

  • 快速问:为什么 c++ 没有指定的求值顺序函数参数?

    一些计算顺序保证了重载运算符和 C++17 中添加的完整参数规则。但仍然没有具体说明哪个论点先行。在 C++17 中,现在指定提供要调用的内容的表达式(函数调用的 ( 的(左侧的代码位于参数之前,并且无论哪个参数先计算,在下一个参数开始之前都会完全计算,在对象方法的情况下,对象的值在方法的参数之前计算。

  • 评价顺序

    21( 括号初始值设定项中逗号分隔的表达式列表中的每个表达式都被计算,就像函数调用(不确定排序(一样

  • 模棱两可的表达式

    C++语言不保证计算函数调用参数的顺序。

在P0145R3。细化惯用语C++的表达式求值顺序 我发现:

后缀表达式的值计算和相关副作用在表达式列表中的表达式之前排序。声明参数的初始化是不确定的顺序,没有交错。

但我没有在标准中找到它,而是在标准中找到:

6.8.1.8 顺序执行 如果与表达式 X 关联的每个值计算和每个副作用都在每个值计算和与表达式 Y 关联的每个副作用之前排序,则表示表达式 X 在表达式 Y 之前排序。

6.8.1.9 顺序执行 与全表达式相关的每个值计算和副作用在与要评估的下一个全表达式相关的每个值计算和副作用之前进行排序。

7.6.19.1 逗号运算符 [expr.comma] 一对用逗号分隔的表达式从左到右计算;

因此,我比较了 14 和 17 标准的三个编译器中的行为。探索的代码是:

#include <iostream>
struct A
{
    A& addInt(int i)
    {
        std::cout << "add int: " << i << "n";
        return *this;
    }
    A& addFloat(float i)
    {
        std::cout << "add float: " << i << "n";
        return *this;
    }
};
int computeInt()
{
    std::cout << "compute intn";
    return 0;
}
float computeFloat()
{
    std::cout << "compute floatn";
    return 1.0f;
}
void compute(float, int)
{
    std::cout << "computen";
}
int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:n";
    compute(computeFloat(), computeInt());
}

结果(更一致的是叮当声(:

<style type="text/css">
  .tg {
    border-collapse: collapse;
    border-spacing: 0;
    border-color: #aaa;
  }
  
  .tg td {
    font-family: Arial, sans-serif;
    font-size: 14px;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #333;
    background-color: #fff;
  }
  
  .tg th {
    font-family: Arial, sans-serif;
    font-size: 14px;
    font-weight: normal;
    padding: 10px 5px;
    border-style: solid;
    border-width: 1px;
    overflow: hidden;
    word-break: normal;
    border-color: #aaa;
    color: #fff;
    background-color: #f38630;
  }
  
  .tg .tg-0pky {
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
  
  .tg .tg-fymr {
    font-weight: bold;
    border-color: inherit;
    text-align: left;
    vertical-align: top
  }
</style>
<table class="tg">
  <tr>
    <th class="tg-0pky"></th>
    <th class="tg-fymr">C++14</th>
    <th class="tg-fymr">C++17</th>
  </tr>
  <tr>
    <td class="tg-fymr"><br>gcc 9.0.1<br></td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">clang 9</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute float<br>compute int<br>compute</td>
  </tr>
  <tr>
    <td class="tg-fymr">msvs 2017</td>
    <td class="tg-0pky">compute int<br>compute float<br>add float: 1<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
    <td class="tg-0pky">compute float<br>add float: 1<br>compute int<br>add int: 0<br>Function call:<br>compute int<br>compute float<br>compute</td>
  </tr>
</table>