【问题标题】:What are the evaluation order guarantees introduced by C++17?C++17 引入的评估顺序保证是什么?
【发布时间】:2016-11-24 21:21:42
【问题描述】:

C++17 evaluation order guarantees (P0145) 中的投票对典型 C++ 代码有何影响?

它对以下内容有何改变?

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

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

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

【问题讨论】:

标签: c++ c++17 operator-precedence


【解决方案1】:

到目前为止,评估顺序未指定的一些常见情况已通过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

后缀表达式从左到右计算。这包括 函数调用和成员选择表达式。

赋值表达式从右到左计算。这 包括复合作业。

移位运算符的操作数从左到右计算。在 总结,以下表达式按 a 的顺序求值,然后 b,然后是 c,然后是 d:

  1. a.b
  2. a->b
  3. a->*b
  4. a(b1, b2, b3)
  5. b @= a
  6. a[b]
  7. 一个
  8. a >> b

此外,我们建议以下附加规则: 涉及重载运算符的表达式的评估是 由与相应内置相关联的顺序确定 运算符,而不是函数调用的规则。

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

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

§5.2.2 - 函数调用 5.2.2.4:

。 . . 后缀表达式在每个表达式之前排序 表达式列表和任何默认参数。每个值计算和 与参数初始化相关的副作用,以及 初始化本身,在每个值计算之前排序,并且 与任何后续初始化相关的副作用 参数。

但是,the 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;

仍未定义。

【讨论】:

  • “第二个附属提案将函数调用的评估顺序替换为如下:函数在其所有参数之前被评估,但任何一对参数(来自参数列表)是不确定的;这意味着一个在另一个之前评估,但没有指定顺序;保证函数在参数之前评估。这反映了核心工作组的一些成员提出的建议。"
  • 我从论文中得到了这样的印象:“以下表达式的计算顺序是 a,然后是 b,然后是 c,然后是 d”,然后显示a(b1, b2, b3),表明所有b 表达式不一定按任何顺序求值(否则为a(b, c, d)
  • @KABoissoneault,你是对的,我已经相应地更新了答案。此外,所有:引号 are 来自版本 3,据我了解,这是已投票的版本。
  • @JohanLundberg 我认为论文中还有另一件事很重要。 a(b1()(), b2()()) 可以按任何顺序订购 b1()()b2()(),但它不能执行 b1() 然后 b2()() 然后 b1()():它可能不再交错执行。简而言之,“8. 函数调用的替代评估顺序”是已批准变更的一部分。
  • f(i++, i) 未定义。现在未指定。 Stroustrup 的字符串示例可能是未指定的,而不是未定义的。 ` f(get_raw_a(),get_raw_a());` 不会编译,因为相关的unique_ptr 构造函数是显式的。最后,x++ + ++x 未定义,句号。
【解决方案2】:

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&lt;A&gt;构造函数
  3. new B
  4. unique_ptr&lt;B&gt;构造函数

这些的顺序是完全未指定的,因此完全有效的顺序是 (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)。未指定采用哪种排序方式,但这两种方式都是安全的。现在禁止所有 (1) (3) 都发生在 (2) 和 (4) 之前的排序。

【讨论】:

  • 有点偏题,但这是 boost::make_shared 和后来的 std::make_shared 的原因之一(另一个原因是更少的分配 + 更好的局部性)。听起来异常安全/资源泄漏的动机不再适用。参见代码示例 3,boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/…Editstackoverflow.com/a/48844115herbsutter.com/2013/05/29/gotw-89-solution-smart-pointers
  • 我想知道这种变化如何影响优化。编译器现在对于如何组合和交错与参数计算相关的 CPU 指令的选项数量已大大减少,因此可能会导致 CPU 利用率降低?
  • obj.modify().f(obj.access()) 的情况如何:obj.modify() 出现在obj.access() 之前还是之后是否定义明确? (听起来至少在obj.modify().f(obj.access().foo()) 中,所有obj.access().foo() 都会“一起”发生,而不是obj.modify()obj.access() 之后在.foo() 之前排序。)
  • @Ben 由top answer 处理。在a.b 中,ab 之前进行评估。
【解决方案3】:

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

  • Quick Q: Why doesn’t c++ have a specified order for evaluating function arguments?

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

  • Order of evaluation

    21) 括号中的初始化程序中以逗号分隔的表达式列表中的每个表达式都被评估为函数调用(indeterminately-sequenced

  • Ambiguous expressions

    C++ 语言不保证函数调用的参数的计算顺序。

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

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

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

6.8.1.8 Sequential execution [intro.execution] 如果与表达式 X 关联的每个值计算和每个副作用都在每个值计算和与表达式 Y 关联的每个副作用之前排序,则表示表达式 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());
}

结果(越一致就是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>

【讨论】:

    猜你喜欢
    • 2013-10-18
    • 2023-03-03
    • 1970-01-01
    • 2011-08-30
    • 2010-09-18
    • 2018-11-20
    • 2015-11-02
    • 1970-01-01
    • 2021-02-14
    相关资源
    最近更新 更多