C++17 引入的评估顺序保证是什么?
C++17 评估顺序保证 (P0145) 中的投票对典型 C++ 代码有何影响?
它对以下内容有何改变?
i = 1;f(i++, i)
和
std::cout <<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(g(), h(), j());
仍有未指定的 g、h 和 j 评估顺序.请注意,对于 getf()(g(),h(),j())
,规则规定 getf()
将在 g 之前计算,h, j
.
还请注意提案文本中的以下示例:
<块引用> std::string s = 但我听说它有效,即使你不相信它"s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(不要"), 6, ");
该示例来自 The C++ Programming Language,第 4 版,Stroustrup,并且曾经是未指定的行为,但在 C++17 中它将按预期工作.可恢复函数 (.then( . . . )
) 也存在类似问题.
作为另一个示例,请考虑以下内容:
#include <iostream>#include <字符串>#include <向量>#include <cassert>结构扬声器{诠释我=0;扬声器(std::vector words):words(words){}标准::向量<标准::字符串>字;标准::字符串运算符()(){断言(words.size()>0);if(i==words.size()) i=0;//C++17 之前的版本:auto word = words[i] + (i+1==words.size()?"
":",");++i;返回词;//使用 C++17 仍然不可能://return words[i++] + (i==words.size()?"
":",");}};int main() {auto spk = Speaker{{All", Work", and", no", play"}};std::cout <<spk() <<spk() <<spk() <<spk() <<spk() ;}
在 C++14 之前,我们可能(并且将会)得到如下结果
播放不,并且,工作,所有,
而不是
All,work,and,no,play
请注意,上面的效果与
((((((std::cout)<<spk())<<spk())<<spk())<<spk())<< spk()) ;
但是,在 C++17 之前,不能保证第一个调用会首先进入流.
参考:来自接受的提案一个>:
<块引用>后缀表达式从左到右计算.这包括函数调用和成员选择表达式.
赋值表达式从右到左计算.这包括复合作业.
移位运算符的操作数从左到右计算.在总结,以下表达式按 a 的顺序求值,然后b,然后是 c,然后是 d:
- a.b
- a->b
- a->*b
- a(b1, b2, b3)
- b @= a
- a[b]
- 一个<<b
- 一个>>b
此外,我们建议以下附加规则:涉及重载运算符的表达式的评估是由与相应内置相关联的顺序确定运算符,而不是函数调用的规则.
编辑说明:我的原始答案误解了 a(b1, b2, b3)
.b1
、b2
、b3
的顺序仍未指定.(谢谢@KABoissonneault,所有评论者.)
但是,(正如@Yakk 指出的那样)这很重要:即使 b1
、b2
、b3
是不平凡的表达式,在开始评估其他参数之前,它们中的每一个都被完全评估并绑定到各自的函数参数.标准是这样规定的:
§5.2.2 - 函数调用 5.2.2.4:
...后缀表达式在每个表达式之前排序表达式列表和任何默认参数.每个值计算和与参数初始化相关的副作用,以及初始化本身,在每个值计算之前排序,并且与任何后续初始化相关的副作用参数.
但是,GitHub 中缺少其中一个新句子草稿:
<块引用>与参数的初始化以及初始化本身是在每个值计算和相关的副作用之前排序任何后续参数的初始化.
示例在那里.它解决了几十年前的问题(正如 Herb Sutter 所解释的那样)具有异常安全性,例如
f(std::unique_ptra, std::unique_ptrb);f(get_raw_a(),get_raw_a());
如果其中一个调用 get_raw_a()
会在另一个调用之前抛出,则会泄漏原始指针与其智能指针参数绑定.
正如 T.C. 所指出的,该示例存在缺陷,因为从原始指针构造 unique_ptr 是显式的,因此无法编译.*
还要注意这个经典的问题(标记为C,而不是 C++):
<块引用>int x=0;x++ + ++x;
仍未定义.
What are the implications of the voted in C++17 evaluation order guarantees (P0145) on typical C++ code?
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
. 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. i++
might be evaluated before i
, or vice-versa. 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). But you will never get f(2, 2)
or anything else of that nature.
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(g(), h(), j());
still has unspecified evaluation order of g, h, and j. Note that for getf()(g(),h(),j())
, the rules state that getf()
will be evaluated before 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. There were similar issues with resumable functions (.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()?"
":",");
++i;
return word;
// Still not possible with C++17:
// return words[i++] + (i==words.size()?"
":",");
}
};
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
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.
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
- a->b
- a->*b
- a(b1, b2, b3)
- b @= a
- a[b]
- a << b
- 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)
. The order of b1
, b2
, b3
is still unspecified. (thank you @KABoissonneault, all commenters.)
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. The standard states this like this:
§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.
However, one of these new sentences are missing from 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.
The example is there. It solves a decades-old problems (as explained by Herb Sutter) with exception safety where things like
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.
As pointed out by T.C., the example is flawed since unique_ptr construction from raw pointer is explicit, preventing this from compiling.*
Also note this classical question (tagged C, not C++):
int x=0; x++ + ++x;
is still undefined.
相关文章