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之前,并不能保证第一次调用会首先进入流。
参考文献:来自accepted proposal:
后缀表达式从左到右进行计算。这包括函数调用和成员选择表达式。
分配表达式从右到左进行评估。这包括复合作业。
从左到右评估移位运算符的操作数。总之,以下表达式按a,然后b,然后是c,然后d来计算:
- A·B
- A-> B
- 一个 - > * B
- a(b1,b2,b3)
- b @ = a
- 一个并[b]
- a << b
- a >> b
此外,我们建议使用以下附加规则:涉及重载运算符的表达式的求值顺序由与相应内置运算符关联的顺序确定,而不是函数调用的规则。
编辑说明:我的原始答案误解了a(b1, b2, b3)
。 b1
,b2
,b3
的顺序仍未明确。 (谢谢@KABoissonneault,所有评论者。)
然而,(正如@Yakk指出的那样)并且这很重要:即使b1
,b2
,b3
是非平凡的表达式,在开始评估其他函数参数之前,它们中的每一个都被完全评估并绑定到相应的函数参数。标准声明如下:
§5.2.2 - 函数调用5.2.2.4:
。 。 。后缀表达式在表达式列表中的每个表达式和任何默认参数之前进行排序。与参数初始化相关联的每个值计算和副作用以及初始化本身在每个值计算和与任何后续参数的初始化相关联的副作用之前被排序。
然而,github draft缺少其中一个新句子:
与参数初始化相关联的每个值计算和副作用以及初始化本身在每个值计算和与任何后续参数的初始化相关联的副作用之前被排序。
就是那个例子。它解决了几十年前的问题(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 ++ 14中,以下内容不安全:
void foo(std::unique_ptr<A>, std::unique_ptr<B> );
foo(std::unique_ptr<A>(new A), std::unique_ptr<B>(new B));
在函数调用期间,此处有四个操作
new A
unique_ptr<A>
建设者new B
unique_ptr<B>
建设者这些的排序是完全未指定的,因此完全有效的排序是(1),(3),(2),(4)。如果选择了这个顺序并且(3)抛出,那么来自(1)的内存泄漏 - 我们还没有运行(2),这将防止泄漏。
在C ++ 17中,新规则禁止交错。来自[intro.execution]:
对于每个函数调用F,对于在F内发生的每个评估A以及在F内未发生但在同一线程上作为同一信号处理程序(如果有)的一部分进行评估的每个评估B,A在B之前被排序或者B在A之前排序。
这句话的脚注如下:
换句话说,函数执行不会相互交错。
这给我们留下了两个有效的排序:(1),(2),(3),(4)或(3),(4),(1),(2)。没有说明采取哪种排序,但这两种都是安全的。现在禁止在(2)和(4)之前发生(1)(3)的所有排序。
我发现了一些关于表达式评估顺序的注释:
在P0145R3.Refining Expression Evaluation Order for Idiomatic C++我发现:
后缀表达式的值计算和相关的副作用在表达式列表中的表达式之前排序。声明的参数的初始化是不确定地排序的,没有交错。
但我没有在标准中找到它,而是在我发现的标准中:
6.8.1.8 Sequential execution [intro.execution]如果在每个值计算和与表达式Y相关联的每个副作用之前对表达式X相关联的每个值计算和每个副作用进行排序,则表达式X被称为在表达式Y之前被排序。
6.8.1.9 Sequential execution [intro.execution]在与要评估的下一个完整表达式相关联的每个值计算和副作用之前,对与完整表达式相关联的每个值计算和副作用进行排序。
7.6.19.1 Comma operator [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 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());
}
结果(铿锵声越一致):
<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>