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

95

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

Что изменится в следующих вещах?

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

а также

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

или

f(g(), h(), j());
Йохан Лундберг
источник
Относится к порядку оценки оператора присваивания в C ++ и имеет ли этот код из раздела 36.3.6 4-го издания «Язык программирования C ++» четко определенное поведение? которые оба рассмотрены в статье. Первый из них может послужить хорошим дополнительным примером в вашем ответе ниже.
Шафик Ягмур

Ответы:

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

Ссылки: Из принятого предложения :

Выражения Postfix оцениваются слева направо. Сюда входят вызовы функций и выражения выбора членов.

Выражения присваивания оцениваются справа налево. Сюда входят составные задания.

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

  1. ab
  2. а-> б
  3. а -> * б
  4. а (b1, b2, b3)
  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()будет сгенерирован до того, как другой необработанный указатель будет привязан к его параметру интеллектуального указателя.

Как указывает 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, которая, насколько я понимаю, проголосовала за версию.
Johan Lundberg
2
@JohanLundberg Есть еще одна важная вещь из статьи. a(b1()(), b2()())может заказать b1()()и b2()()в любом порядке, но он не может делать b1()то b2()()тогда b1()(): он может больше не перемежать их казни. Короче говоря, «8. АЛЬТЕРНАТИВНЫЙ ПОРЯДОК ОЦЕНКИ ФУНКЦИОНАЛЬНЫХ ЗВОНКОВ» был частью утвержденного изменения.
Yakk - Adam Nevraumont
3
f(i++, i)был неопределенным. Сейчас это не указано. Пример строки Страуструпа, вероятно, был неопределенным, а не неопределенным. `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 новые правила запрещают чередование. Из [intro.execution]:

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

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

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

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

Барри
источник
1
Небольшое отступление, но это была одна из причин для boost :: make_shared, а затем std :: make_shared (другая причина - меньшее количество выделений + лучшая локальность). Похоже, мотивация исключительной безопасности / утечки ресурсов больше не применяется. См. Пример кода 3, boost.org/doc/libs/1_67_0/libs/smart_ptr/doc/html/… Edit и stackoverflow.com/a/48844115 , grassutter.com/2013/05/29/gotw-89-solution- smart-указатели
Макс Барраклаф
3
Интересно, как это изменение влияет на оптимизацию. Компилятор теперь значительно сократил количество опций относительно того, как комбинировать и чередовать инструкции ЦП, связанные с вычислением аргументов, так что это может привести к более низкой загрузке ЦП?
Violet Giraffe
2

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

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

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

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

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

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

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

В P0145R3.Refining Expression Evaluation Order для идиоматического C ++ я обнаружил:

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

Но я не нашел его в стандарте, вместо этого в стандарте я нашел:

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>

lvccgd
источник