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

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

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

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

а также

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

или же

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

Ответ 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, "");

Этот пример взят из языка программирования 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. a-> б
  3. a-> * б
  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;

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

Ответ 2

Перемежение запрещено в С++ 17

В С++ 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), что предотвратило бы утечку.


В С++ 17 новые правила запрещают чередование. Из [intro.execution]:

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

Есть сноска к этому предложению, которое гласит:

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

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

Ответ 3

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

  • Быстрый вопрос: Почему 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>