Які гарантії замовлення на оцінку вводить 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 четвертого видання “Мова програмування 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, "");

Приклад походить із мови програмування 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()) ;

Але все-таки до С ++ 17 не було жодної гарантії, що перші дзвінки прийдуть першими в потік.

Список літератури: З прийнятої пропозиції :

Вирази постфіксу обчислюються зліва направо. Це включає виклики функцій та вирази вибору членів.

Вирази призначення призначаються справа наліво. Сюди входять складні завдання.

Операнди для операторів зсуву обчислюються зліва направо. Підсумовуючи, наступні вирази обчислюються в порядку a, потім b, потім c, потім d:

  1. ab
  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). Порядок b1, b2, b3до сих пір НЕ визначено. (дякую @KABoissonneault, всі коментатори.)

Однак, (як @Yakk вказує) , і це дуже важливо: Навіть коли b1, b2, b3нетривіальні вирази, кожен з них повністю оцінені і пов'язані з відповідним параметром функції До початку бути оцінені іншими. Стандарт зазначає це так:

§ 5.2.2 - Виклик функції 5.2.2.4:

. . . Вираз postfix секвенується перед кожним виразом у списку виразів та будь-яким аргументом за замовчуванням. Кожне обчислення значення та побічний ефект, пов’язаний з ініціалізацією параметра, та сама ініціалізація послідовно розподіляються перед кожним обчисленням значення та побічним ефектом, пов’язаним з ініціалізацією будь-якого наступного параметра.

Однак одне з цих нових речень відсутнє в проекті 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
"Друга допоміжна пропозиція замінює порядок оцінки викликів функції таким чином: функція обчислюється перед усіма її аргументами, але будь-яка пара аргументів (зі списку аргументів) є невизначеною послідовністю; це означає, що один обчислюється перед іншим, але порядок не вказаний; гарантується, що функція оцінюється перед аргументами. Це відображає пропозицію деяких членів Основної робочої групи. "
Якк - Адам Неврамон

1
Я отримую таке враження від статті, в якій сказано, що "наступні вирази оцінюються в порядку a, потім b, потім c, потім d", а потім показується a(b1, b2, b3), припускаючи, що всі bвирази не обов'язково оцінюються в будь-якому порядку (інакше це було б a(b, c, d))
KABoissonneault

1
@KABoissoneault, Ви праві, і я відповідно оновив відповідь. Крім того, все: лапки - це версія 3, яка, наскільки я розумію, є голосованою у цій версії.
Йоган Лундберг,

2
@JohanLundberg Є ще одна річ із паперу, яку я вважаю важливою. a(b1()(), b2()())можна замовити b1()()і b2()()в будь-якому порядку, але він не може зробитиb1() те b2()()тоді b1()(): він може більше не змішувати їх страти. Коротше кажучи, "8. ПІДМІННИЙ ПОРЯДОК ОЦІНКИ ДЛЯ ФУНКЦІОНАЛЬНИХ ЗВІН" був частиною схваленої зміни.
Якк - Адам Неврамон

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 , herbsutter.com/2013/05/29/gotw-89-solution- розумні покажчики
Макс Барракло

3
Цікаво, як ця зміна впливає на оптимізацію. Тепер компілятор значно зменшив кількість варіантів, як комбінувати та чергувати інструкції процесора, пов’язані з обчисленням аргументів, тож це може призвести до слабшого використання центрального процесора?
Вайолет Жираф

2

Я знайшов кілька приміток щодо порядку оцінки виразів:

  • Швидке запитання: Чому у c ++ немає вказаного порядку оцінки аргументів функції?

    Деякі порядки оцінки гарантують оточення перевантажених операторів та правил повного аргументу, де це додано в C ++ 17. Але залишається, що аргумент, який йде першим, залишається невизначеним. У C ++ 17 тепер зазначено, що вираз, що викликає, що викликати (код ліворуч від (виклику функції) йде перед аргументами, і будь-який аргумент, який обчислюється першим, обчислюється повністю перед наступним і у випадку методу об'єкта значення об'єкта обчислюється перед аргументами методу.

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

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

  • Неоднозначні вирази

    Мова С ++ не гарантує порядок, в якому обчислюються аргументи виклику функції.

У P0145R3. Визначення порядку оцінки виразу для ідіоматичного 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>

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.