Какие гарантии порядка оценки введены в C++17?

Каковы последствия использования утвержденного порядка оценки C++17 (P0145) для типичного кода C++?

Что это меняет в таких вещах, как

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

а также

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

или же

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

2 ответа

Решение

Некоторые распространенные случаи, когда порядок оценки до сих пор не определен, указаны и действительны с C++17, Некоторое неопределенное поведение теперь не указано.

Как насчет таких вещей, как

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

был неопределен, но теперь не указан.

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, "");

Пример взят из языка программирования C++, 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 не было никакой гарантии, что первые вызовы будут первыми в потоке.

Рекомендации: Из принятого предложения:

Постфиксные выражения оцениваются слева направо. Это включает вызовы функций и выражения выбора членов.

Выражения присваивания оцениваются справа налево. Это включает в себя составные задания.

Операнды для смещения операторов оцениваются слева направо. Таким образом, следующие выражения оцениваются в порядке a, затем b, затем c, затем d:

  1. аб
  2. а-> Ь
  3. а ->* б
  4. а (б1, б2, б3)
  5. б @= а
  6. а [Ь]
  7. а << б
  8. а >> б

Кроме того, мы предлагаем следующее дополнительное правило: порядок вычисления выражения, включающего перегруженный оператор, определяется порядком, связанным с соответствующим встроенным оператором, а не правилами для вызовов функций.

Редактировать заметку: мой оригинальный ответ неверно истолкован a(b1, b2, b3), Получатель чего-то b1, b2, b3 все еще не определено. (спасибо @KABoissonneault, все комментаторы.)

Тем не менее, (как указывает @Yakk), и это важно: даже когда b1, b2, b3 являются нетривиальными выражениями, каждое из них полностью вычисляется и привязывается к соответствующему параметру функции до того, как начнут оцениваться другие. Стандарт утверждает это следующим образом:

§5.2.2 - Вызов функции 5.2.2.4:

,,, Постфиксное выражение упорядочивается перед каждым выражением в списке выражений и любым аргументом по умолчанию. Каждое вычисление значения и побочный эффект, связанный с инициализацией параметра и самой инициализацией, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с инициализацией любого последующего параметра.

Тем не менее, одно из этих новых предложений отсутствует в проекте github:

Каждое вычисление значения и побочный эффект, связанный с инициализацией параметра и самой инициализацией, упорядочивается перед каждым вычислением значения и побочным эффектом, связанным с инициализацией любого последующего параметра.

Пример есть. Это решает давние проблемы ( как объяснил Херб Саттер) с исключительной безопасностью, где такие вещи, как

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

f(get_raw_a(),get_raw_a()); 

протекает, если один из звонков get_raw_a() бросил бы прежде, чем другой необработанный указатель был привязан к его параметру умного указателя edit: как указано в TC, пример ошибочен, так как конструкция unique_ptr из необработанного указателя является явной, что препятствует компиляции.

Также обратите внимание на этот классический вопрос (помеченный C, а не C++):

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

все еще не определено.

Чередование запрещено в 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 новые правила запрещают чередование. Из [intro.execution]:

Для каждого вызова функции F, для каждой оценки A, которая происходит в пределах F, и каждой оценки B, которая не происходит в пределах F, но оценивается в том же потоке и как часть одного и того же обработчика сигнала (если таковой имеется), либо A последовательно перед B или B секвенируется перед A.

В этом предложении есть сноска, которая гласит:

Другими словами, выполнение функций не чередуется друг с другом.

Это оставляет нам два действительных порядка: (1), (2), (3), (4) или (3), (4), (1), (2). Не указано, какой порядок принят, но оба они безопасны. Все упорядочения, в которых (1) (3) происходят раньше (2) и (4), теперь запрещены.

Я нашел несколько заметок о порядке вычисления выражений:

  • Быстрый вопрос: Почему в C++ нет определенного порядка для оценки аргументов функции?

    Некоторый порядок оценки гарантирует, что перегруженные операторы и правила полного аргумента добавлены в C++17. Но остается то, что аргумент идет первым остается не определенным. В C++ 17 теперь указывается, что выражение, дающее то, что вызывать (код слева от (вызова функции), идет перед аргументами, и любой аргумент, вычисляемый первым, вычисляется полностью, прежде чем следующий запущен, и в случае метода объекта значение объекта оценивается до того, как аргументы метода будут.

  • Порядок оценки

    21) Каждое выражение в списке выражений, разделенных запятыми, в инициализаторе, заключенном в скобки, оценивается как для вызова функции (неопределенно-упорядоченный)

  • Неоднозначные выражения

    Язык C++ не гарантирует порядок, в котором оцениваются аргументы для вызова функции.

В P0145R3.Refining Порядок оценки выражений для Idiomatic C++ я нашел:

Вычисление значения и связанный с ним побочный эффект выражения postfix упорядочиваются раньше, чем выражения в списке выражений. Инициализация объявленных параметров выполняется неопределенным образом без чередования.

Но я не нашел его в стандарте, а в стандарте, который нашел:

6.8.1.8 Последовательное выполнение [intro.execution] Говорят, что выражение X упорядочено перед выражением Y, если каждое вычисление значения и каждый побочный эффект, связанный с выражением X, упорядочивается перед каждым вычислением значения и каждым побочным эффектом, связанным с выражением Y,

6.8.1.9 Последовательное выполнение [intro.execution] Каждое вычисление значения и побочный эффект, связанный с полным выражением, секвенируются перед каждым вычислением значения и побочным эффектом, связанным со следующим полным выражением, которое будет оценено.

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

Результаты (более последовательный лязг):

<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>

Другие вопросы по тегам