简体   繁体   English

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

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

What are the implications of the voted in C++17 evaluation order guarantees (P0145) on typical C++ code?C++17 评估顺序保证 (P0145) 中投票对典型 C++ 代码有什么影响?

What does it change about things like the following?以下内容有什么变化?

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

and

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

or或者

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

Some common cases where the evaluation order has so far been unspecified , are specified and valid with C++17 .到目前为止尚未指定求值顺序的一些常见情况在C++17已指定且有效。 Some undefined behaviour is now instead unspecified.一些未定义的行为现在改为未指定。

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

was undefined, but it is now unspecified.未定义,但现在未指定。 Specifically, what is not specified is the order in which each argument to f is evaluated relative to the others.具体来说,未指定的是f每个参数相对于其他参数的评估顺序。 i++ might be evaluated before i , or vice-versa. i++可能在i之前被评估,反之亦然。 Indeed, it might evaluate a second call in a different order, despite being under the same compiler.事实上,尽管在同一个编译器下,它可能会以不同的顺序评估第二个调用。

However, the evaluation of each argument is required to execute completely, with all side-effects, before the execution of any other argument.但是,在执行任何其他参数之前,需要对每个参数的评估完全执行,并产生所有副作用。 So you might get f(1, 1) (second argument evaluated first) or f(1, 2) (first argument evaluated first).因此,您可能会得到f(1, 1) (首先评估第二个参数)或f(1, 2) (首先评估第一个参数)。 But you will never get f(2, 2) or anything else of that nature.但是你永远不会得到f(2, 2)或任何其他性质的东西。

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

was unspecified, but it will become compatible with operator precedence so that the first evaluation of f will come first in the stream (examples below).未指定,但它将与运算符优先级兼容,因此f的第一个评估将首先出现在流中(下面的示例)。

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

still has unspecified evaluation order of g, h, and j.仍然有未指定的 g、h 和 j 的评估顺序。 Note that for getf()(g(),h(),j()) , the rules state that getf() will be evaluated before g, h, j .请注意,对于getf()(g(),h(),j()) ,规则规定getf()将在g, h, j之前进行评估。

Also note the following example from the proposal text:另请注意提案文本中的以下示例:

 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 example comes from The C++ Programming Language , 4th edition, Stroustrup, and used to be unspecified behaviour, but with C++17 it will work as expected.该示例来自The C++ Programming Language ,第 4 版,Stroustrup,并且曾经是未指定的行为,但使用 C++17 它将按预期工作。 There were similar issues with resumable functions ( .then( . . . ) ).可恢复函数也有类似的问题( .then( . . . ) )。

As another example, consider the following:作为另一个示例,请考虑以下内容:

#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() ;
}

With C++14 and before we may (and will) get results such as使用 C++14 和之前我们可能(并且将会)得到诸​​如

play
no,and,Work,All,

instead of代替

All,work,and,no,play

Note that the above is in effect the same as请注意,上述内容实际上与

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

But still, before C++17 there was no guarantee that the first calls would come first into the stream.但是,在 C++17 之前,不能保证第一个调用会首先进入流。

References: From 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:总之,以下表达式的计算顺序是 a,然后是 b,然后是 c,然后是 d:

  1. ab AB
  2. a->b a->b
  3. a->*b a->*b
  4. a(b1, b2, b3) a(b1, b2, b3)
  5. b @= a b @= a
  6. a[b] [b]
  7. a << b一<<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.此外,我们建议以下附加规则:涉及重载运算符的表达式的计算顺序由与相应内置运算符关联的顺序决定,而不是由函数调用规则决定。

Edit note: My original answer misinterpreted a(b1, b2, b3) .编辑说明:我的原始答案误解a(b1, b2, b3) The order of b1 , b2 , b3 is still unspecified. b1b2b3的顺序仍未指定。 (thank you @KABoissonneault, all commenters.) (感谢@KABOissonneault,所有评论者。)

However, (as @Yakk points out) and this is important: Even when b1 , b2 , b3 are non-trivial expressions, each of them are completely evaluated and tied to the respective function parameter before the other ones are started to be evaluated.但是,(正如@Yakk 指出的那样)这很重要:即使b1b2b3是非平凡的表达式,它们中的每一个也会被完全评估在开始评估其他函数参数之前与各自的函数参数相关联 The standard states this like this:标准是这样规定的:

§5.2.2 - Function call 5.2.2.4: §5.2.2 - 函数调用 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.与参数初始化相关的每个值计算和副作用,以及初始化本身,都在与任何后续参数初始化相关的每个值计算和副作用之前进行排序。

However, one of these new sentences are missing from the GitHub draft :但是, GitHub 草案中缺少以下新句子之一:

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 example is there.例子在那里。 It solves a decades-old problems ( as explained by Herb Sutter ) with exception safety where things like它解决了几十年前的问题( 如 Herb Sutter 所解释的),并具有异常安全性,例如

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

f(get_raw_a(), get_raw_a());

would leak if one of the calls get_raw_a() would throw before the other raw pointer was tied to its smart pointer parameter.如果其中一个调用get_raw_a()在另一个原始指针绑定到其智能指针参数之前抛出,则会泄漏。

As pointed out by TC, the example is flawed since unique_ptr construction from raw pointer is explicit, preventing this from compiling.*正如 TC 所指出的,该示例存在缺陷,因为从原始指针构建的 unique_ptr 是显式的,从而阻止了编译。*

Also note this classical question (tagged C , not C++ ):还要注意这个经典问题(标记为C ,而不是C++ ):

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

is still undefined.仍然未定义。

Interleaving is prohibited in C++17 C++17 中禁止交错

In C++14, the following was unsafe:在 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));

There are four operations that happen here during the function call在函数调用过程中,这里发生了四个操作

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

The ordering of these was completely unspecified, and so a perfectly valid ordering is (1), (3), (2), (4).这些的顺序是完全未指定的,因此完全有效的顺序是 (1)、(3)、(2)、(4)。 If this ordering was selected and (3) throws, then the memory from (1) leaks - we haven't run (2) yet, which would've prevented the leak.如果选择了这个排序并且 (3) 抛出,那么来自 (1) 的内存泄漏——我们还没有运行 (2),这将防止泄漏。


In C++17, the new rules prohibit interleaving.在 C++17 中,新规则禁止交错。 From [intro.execution]:来自 [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.对于每个函数调用 F,对于在 F 中发生的每个求值 A 和不在 F 中但在同一线程上作为同一信号处理程序(如果有)的一部分求值的每个求值 B,A 都在 B 之前排序或 B 在 A 之前排序。

There is a footnote to that sentence which reads:这句话有一个脚注,内容如下:

In other words, function executions do not interleave with each other.换句话说,函数执行不会相互交错。

This leaves us with two valid orderings: (1), (2), (3), (4) or (3), (4), (1), (2).这给我们留下了两个有效的排序:(1)、(2)、(3)、(4) 或 (3)、(4)、(1)、(2)。 It is unspecified which ordering is taken, but both of these are safe.未指定采用哪种排序方式,但这两种方式都是安全的。 All the orderings where (1) (3) both happen before (2) and (4) are now prohibited.现在禁止 (1) (3) 都发生在 (2) 和 (4) 之前的所有排序。

I've found some notes about expression evaluation order:我发现了一些关于表达式求值顺序的注释:

  • Quick Q: Why doesn't c++ have a specified order for evaluating function arguments? 快速问:为什么 c++ 没有指定的顺序来评估函数参数?

    Some order of evaluation guarantees surrounding overloaded operators and complete-argument rules where added in C++17.某些求值顺序保证了 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.在 C++17 中,现在指定给出调用内容的表达式(函数调用左侧的代码)在参数之前,并且首先计算的参数在下一个参数之前完全计算开始,并且在对象方法的情况下,对象的值在方法的参数之前被评估。

  • 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 ) 21) 括号中的初始值设定项中以逗号分隔的表达式列表中的每个表达式都像函数调用一样求值(不确定顺序

  • Ambiguous expressions 含糊不清的表达

    The C++ language does not guarantee the order in which arguments to a function call are evaluated. C++ 语言不保证对函数调用的参数求值的顺序。

In P0145R3.Refining Expression Evaluation Order for Idiomatic C++ I've found: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.声明参数的初始化是不确定的,没有交错。

But I didn't find it in standard, instead in standard I've found:但是我没有在标准中找到它,而是在我发现的标准中:

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.8 顺序执行 [intro.execution]如果每次值计算和与表达式 X 关联的每个副作用都在每次值计算和与表达式 Y 关联的每个副作用之前排序,则称表达式 X 在表达式 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. 6.8.1.9 顺序执行 [intro.execution]与完整表达式相关的每个值计算和副作用在与要评估的下一个完整表达式相关的每个值计算和副作用之前被排序。

7.6.19.1 Comma operator [expr.comma] A pair of expressions separated by a comma is evaluated left-to-right;... 7.6.19.1 逗号运算符 [expr.comma] 从左到右计算一对由逗号分隔的表达式;...

So, I compared according behavior in three compilers for 14 and 17 standards.因此,我比较了 14 和 17 标准的三个编译器的相应行为。 The explored code is:探索的代码是:

#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());
}

Results (the more consistent is clang):结果(更一致的是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>

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM