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


95

在典型的C ++代码中,C ++ 17评估顺序保证(P0145)中投票的含义是什么?

如下所示,它有什么变化?

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

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

要么

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

C ++中的赋值语句的评估顺序有关,并且此“ C ++编程语言”第4版第36.3.6节中的代码是否具有定义明确的行为?两者都在本文中涵盖。第一个可能在下面的答案中提供了一个很好的其他示例。
Shafik Yaghmour

Answers:


83

到目前为止,尚未指定评估顺序的一些常见情况已通过指定并有效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 ++编程语言,第四版,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. 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:

。。。postfix-expression在expression-list中的每个表达式和任何默认参数之前进行排序。与参数的初始化相关联的每个值计算和副作用以及初始化本身在与任何后续参数的初始化相关联的每个值计算和副作用之前进行排序。

但是,GitHub草稿中缺少其中一个新句子:

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

这个例子在那里。它解决了数十年来存在的异常安全问题(如Herb Sutter所述),

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

f(get_raw_a(), get_raw_a());

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

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

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

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

仍未定义。


1
“第二个附属提案按如下方式替换了函数调用的求值顺序:该函数在其所有参数之前进行求值,但是任意一对参数(来自参数列表)都不确定地排序;这意味着一个函数在另一个参数之前求值,但在没有指定顺序;保证在参数之前对函数求值。这反映了核心工作组某些成员的建议。”
Yakk-Adam Nevraumont

1
我从论文中得到的印象是,“以下表达式是按先后顺序a,然后b,然后c,然后d”然后显示的a(b1, b2, b3),这表明所有b表达式不一定按任何顺序求值(否则,将是a(b, c, d)
KABoissonneault

1
@KABoissoneault,您是正确的,我已经相应更新了答案。同样,所有:引号第3版表格,据我所知,这是第3版投票的版本。
约翰·伦德伯格

2
@JohanLundberg论文中还有另一件事我认为很重要。 a(b1()(), b2()())可以订购b1()(),并b2()()以任意顺序,但它不能做的b1(),然后b2()()b1()():它可能不再交错他们处决。简而言之,“ 8。功能调用的替代评估命令”是已批准的更改的一部分。
Yakk-Adam Nevraumont

3
f(i++, i)未定义。现在未指定。Stroustrup的字符串示例可能未指定,但未定义。f(get_raw_a(),get_raw_a());不会编译,因为相关的unique_ptr构造函数是显式的。最后,x++ + ++x是未定义的时期。
TC

44

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,A都在B之前排序。或B在A之前排序。

该句子的脚注如下:

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

这给我们留下了两个有效的顺序:(1),(2),(3),(4)或(3),(4),(1),(2)。尚不确定采用哪种订购方式,但两者都是安全的。现在禁止所有在(2)和(4)之前都出现(1)(3)的命令。


1
稍作保留,但这是boost :: make_shared和后来的std :: make_shared的原因之一(其他原因是分配更少+更好的局部性)。听起来异常安全/资源泄漏的动机不再适用。见代码示例3,boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/... 编辑stackoverflow.com/a/48844115herbsutter.com/2013/05/29/gotw-89-solution-智能指针
Max Barraclough

3
我想知道这种变化如何影响优化。现在,编译器已大大减少了有关如何组合和交织与自变量计算相关的CPU指令的选项,因此可能导致CPU利用率降低吗?
紫罗兰色长颈鹿

2

我发现了一些有关表达式求值顺序的注意事项:

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

    某些评估顺序可以保证在C ++ 17中添加重载运算符和完整参数规则。但是,仍然没有确定先争论哪个。在C ++ 17中,现在指定了给出要调用的内容的表达式((函数调用的)左侧的代码在参数之前,并且首先计算的哪个参数在下一个参数之前被完全求值。开始,对于对象方法,在计算方法参数之前先评估对象的值。

  • 评估顺序

    21)用括号分隔的初始值设定项中用逗号分隔的表达式列表中的每个表达式都像对函数调用那样进行评估(不确定地排序

  • 模棱两可的表达

    C ++语言不保证对函数调用的参数进行评估的顺序。

P0145R3中,为惯用C ++定义表达式评估顺序时,我发现:

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

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

6.8.1.8顺序执行[简介执行] 如果在每次值计算和与表达式Y相关的每个副作用之前对每个值计算和与该表达式X相关的每个副作用进行排序,表达式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 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>

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.