C++17 引入的求值顺序保证是什么?

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

C++17 evaluation order guarantees (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,第 4 版,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 之前,仍然不能保证第一个调用首先进入流。

参考资料:来自the accepted proposal

Postfix expressions are evaluated from left to right. This includes functions calls and member selection expressions.

Assignment expressions are evaluated from right to left. This includes compound assignments.

Operands to shift operators are evaluated from left to right. In summary, the following expressions are evaluated in the order a, then b, then c, then 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

Furthermore, we suggest the following additional rule: the order of evaluation of an expression involving an overloaded operator is determined by the order associated with the corresponding built-in operator, not the rules for function calls.

编辑说明:我原来的回答被误解了a(b1, b2, b3)b1b2b3的顺序仍未确定。 (谢谢@KABoissonneault,所有评论者。)

但是,(正如@Yakk 指出的那样)这很重要:即使 b1b2b3 是不平凡的表达式,它们中的每一个都被完全评估 并绑定到相应的函数参数 ,然后再开始评估其他参数。标准是这样规定的:

§5.2.2 - Function call 5.2.2.4:

. . . The postfix-expression is sequenced before each expression in the expression-list and any default argument. Every value computation and side effect associated with the initialization of a parameter, and the initialization itself, is sequenced before every value computation and side effect associated with the initialization of any subsequent parameter.

但是,the GitHub draft 中缺少其中一个新句子:

Every value computation and side effect associated with the initialization of a parameter, and the initialization itself, is sequenced before every value computation and side effect associated with the initialization of any subsequent parameter.

例子。它解决了一个存在数十年之久的问题 (as explained by Herb Sutter),并且具有异常安全性,例如

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 原始指针的构造是显式的,因此无法编译。*

还要注意这个经典的question(标记为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 中,新规则禁止交错。来自 [intro.execution]:

For each function invocation F, for every evaluation A that occurs within F and every evaluation B that does not occur within F but is evaluated on the same thread and as part of the same signal handler (if any), either A is sequenced before B or B is sequenced before A.

这句话有一个脚注:

In other words, function executions do not interleave with each other.

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

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

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

    Some order of evaluation guarantees surrounding overloaded operators and complete-argument rules where added in C++17. But it remains that which argument goes first is left unspecified. In C++17, it is now specified that the expression giving what to call (the code on the left of the ( of the function call) goes before the arguments, and whichever argument is evaluated first is evaluated fully before the next one is started, and in the case of an object method the value of the object is evaluated before the arguments to the method are.

  • Order of evaluation

    21) Every expression in a comma-separated list of expressions in a parenthesized initializer is evaluated as if for a function call (indeterminately-sequenced)

  • Ambiguous expressions

    The C++ language does not guarantee the order in which arguments to a function call are evaluated.

P0145R3.Refining Expression Evaluation Order for Idiomatic C++ 我发现:

The value computation and associated side-effect of the postfix-expression are sequenced before those of the expressions in the expression-list. The initializations of the declared parameters are indeterminately sequenced with no interleaving.

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

6.8.1.8 Sequential execution [intro.execution] An expression X is said to be sequenced before an expression Y if every value computation and every side effect associated with the expression X is sequenced before every value computation and every side effect associated with the expression Y.

6.8.1.9 Sequential execution [intro.execution] Every value computation and side effect associated with a full-expression is sequenced before every value computation and side effect associated with the next full-expression to be evaluated.

7.6.19.1 Comma operator [expr.comma] A pair of expressions separated by a comma is evaluated left-to-right;...

因此,我针对 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 int\n";
    return 0;
}

float computeFloat()
{
    std::cout << "compute float\n";
    return 1.0f;
}

void compute(float, int)
{
    std::cout << "compute\n";
}

int main()
{
    A a;
    a.addFloat(computeFloat()).addInt(computeInt());
    std::cout << "Function call:\n";
    compute(computeFloat(), computeInt());
}

结果(比较一致的是clang):

<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>