Чи має цей код із розділу 36.3.6 четвертого видання “Мова програмування C ++” чітко визначену поведінку?


94

У розділі STL-подібних операцій 4-го випуску мови програмування на мові програмування C ++ Бьярна Струструпа такий код використовується як приклад ланцюжка :36.3.6

void f2()
{
    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, "" );

    assert( s == "I have heard it works only if you believe in it" ) ;
}

Ствердження не вдається gcc( перегляньте його в прямому ефірі ) та Visual Studio( перегляньте його в прямому ефірі ), але воно не зазнає помилки при використанні Clang ( перегляньте його в прямому ефірі ).

Чому я отримую різні результати? Чи хтось із цих компіляторів неправильно оцінює ланцюговий вираз, чи цей код демонструє певну форму невизначеної чи невизначеної поведінки ?


Краще:s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
Бен Войгт

20
помилка в сторону, хіба я єдиний, хто вважає, що такий потворний код не повинен бути в книзі?
Каролі Горват,

5
@KarolyHorvath Зверніть увагу, що cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c)лише незначно менш потворний.
Oktalist

1
@Oktalist: :) принаймні я маю намір там. він одночасно навчає залежного від аргументів пошуку імені та синтаксису оператора у стислому форматі ... і не створюється враження, що ви насправді повинні писати такий код.
Karoly Horvath

Відповіді:


104

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

Цей приклад згадується у пропозиції N4228: Уточнення порядку оцінки виразів для Idiomatic C ++, де сказано наступне про код у питанні:

[...] Цей код був розглянутий експертами С ++ у всьому світі та опублікований (Мова програмування С ++, 4- е видання.) Проте його вразливість до невизначеного порядку оцінки була виявлена ​​лише нещодавно інструментом [.. .]

Деталі

Багатьом може бути очевидним, що аргументи функцій мають невизначений порядок оцінки, але, мабуть, не настільки очевидно, як ця поведінка взаємодіє з викликами ланцюжкових функцій. Для мене це було не очевидно, коли я вперше проаналізував цю справу, і, мабуть, не для всіх експертів-рецензентів .

На перший погляд може здатися, що оскільки кожна replaceповинна оцінюватися зліва направо, відповідні групи аргументів функції також повинні оцінюватися як групи зліва направо.

Це неправильно, аргументи функції мають невизначений порядок оцінки, хоча ланцюгові виклики функцій вводять порядок оцінки зліва направо для кожного виклику функції, аргументи кожного виклику функції лише секвендуються раніше щодо виклику функції-члена, частиною якого вони є. з. Зокрема, це впливає на такі дзвінки:

s.find( "even" )

і:

s.find( " don't" )

які мають невизначену послідовність щодо:

s.replace(0, 4, "" )

два findвиклики можуть бути оцінені до або після replace, що має значення, оскільки він має побічний ефект sтаким чином, щоб змінити результат find, він змінює тривалість s. Отже, залежно від того, коли replaceце оцінюється щодо двох findдзвінків, результат буде відрізнятися.

Якщо ми подивимося на ланцюговий вираз і вивчимо порядок оцінки деяких підвиразів:

s.replace(0, 4, "" ).replace( s.find( "even" ), 4, "only" )
^ ^       ^  ^  ^    ^        ^                 ^  ^
A B       |  |  |    C        |                 |  |
          1  2  3             4                 5  6

і:

.replace( s.find( " don't" ), 6, "" );
 ^        ^                   ^  ^
 D        |                   |  |
          7                   8  9

Зауважте, ми ігноруємо той факт, що 4і 7можемо далі розбити на більше підвиразів. Так:

  • Aсеквенується перед Bяким секвенується перед Cяким секвенується ранішеD
  • 1до 9невизначеної послідовності щодо інших підвиразів з деякими винятками, переліченими нижче
    • 1до 3яких секвенуються ранішеB
    • 4до 6яких секвенуються ранішеC
    • 7до 9яких секвенуються ранішеD

Ключ до цього питання полягає в тому, що:

  • 4до 9невизначеної послідовності щодоB

Потенційний порядок вибору оцінки для 4і 7стосовно Bпояснює різницю в результатах між оцінюванням clangта gccпід час оцінювання f2(). У моїх тестах clangоцінює Bперед оцінюванням 4і 7під час gccоцінки після. Ми можемо використовувати наступну тестову програму, щоб продемонструвати, що відбувається в кожному конкретному випадку:

#include <iostream>
#include <string>

std::string::size_type my_find( std::string s, const char *cs )
{
    std::string::size_type pos = s.find( cs ) ;
    std::cout << "position " << cs << " found in complete expression: "
        << pos << std::endl ;

    return pos ;
}

int main()
{
   std::string s = "but I have heard it works even if you don't believe in it" ;
   std::string copy_s = s ;

   std::cout << "position of even before s.replace(0, 4, \"\" ): " 
         << s.find( "even" ) << std::endl ;
   std::cout << "position of  don't before s.replace(0, 4, \"\" ): " 
         << s.find( " don't" ) << std::endl << std::endl;

   copy_s.replace(0, 4, "" ) ;

   std::cout << "position of even after s.replace(0, 4, \"\" ): " 
         << copy_s.find( "even" ) << std::endl ;
   std::cout << "position of  don't after s.replace(0, 4, \"\" ): "
         << copy_s.find( " don't" ) << std::endl << std::endl;

   s.replace(0, 4, "" ).replace( my_find( s, "even" ) , 4, "only" )
        .replace( my_find( s, " don't" ), 6, "" );

   std::cout << "Result: " << s << std::endl ;
}

Результат для gcc( дивіться в прямому ефірі )

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26

Result: I have heard it works evenonlyyou donieve in it

Результат для clang( дивіться в прямому ефірі ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position even found in complete expression: 22
position don't found in complete expression: 33

Result: I have heard it works only if you believe in it

Результат для Visual Studio( дивіться в прямому ефірі ):

position of even before s.replace(0, 4, "" ): 26
position of  don't before s.replace(0, 4, "" ): 37

position of even after s.replace(0, 4, "" ): 22
position of  don't after s.replace(0, 4, "" ): 33

position  don't found in complete expression: 37
position even found in complete expression: 26
Result: I have heard it works evenonlyyou donieve in it

Деталі зі стандарту

Ми знаємо, що якщо не вказано, оцінки підвиразів не виконуються, це з проекту стандартного розділу C ++ 11 1.9 Виконання програми, який говорить:

За винятком випадків, коли зазначено, оцінки операндів окремих операторів та підвиразів окремих виразів не є послідовними. [...]

і ми знаємо, що виклик функції вводить секвенуваний до відношення виклику функції вираз postfix та аргументи щодо тіла функції, з розділу 1.9:

[...] Під час виклику функції (незалежно від того, функція вбудована), кожне обчислення значення та побічний ефект, пов’язаний з будь-яким виразом аргументу, або з виразом постфіксу, що позначає викликану функцію, секвенується перед виконанням кожного виразу чи оператора в тілі викликаної функції. [...]

Ми також знаємо, що доступ членів класу і, отже, ланцюжок буде оцінювати зліва направо, з розділу 5.2.5 Доступ члена класу, який говорить:

[...] Вираховується вираз постфікса перед крапкою або стрілкою; 64 результат цієї оцінки, разом із виразом id, визначає результат всього виразу postfix.

Зверніть увагу, в тому випадку , коли Ід вираз закінчує тим , що ні-статична функція члена вона не визначає порядок оцінки спісок_вираженій всередині , ()так як це окрема подвираженія. Відповідна граматика з 5.2 виразів Postfix :

postfix-expression:
    postfix-expression ( expression-listopt)       // function call
    postfix-expression . templateopt id-expression // Class member access, ends
                                                   // up as a postfix-expression

Зміни в C ++ 17

Пропозиція p0145r3: Уточнення порядку оцінки виразів для Idiomatic C ++ внесла кілька змін. Включаючи зміни, які надають коду чітко визначену поведінку шляхом посилення порядку правил оцінки виразів postfix та їх списку виразів .

[expr.call] p5 говорить:

Вираз postfix секвенується перед кожним виразом у списку виразів та будь-яким аргументом за замовчуванням . Ініціалізація параметра, включаючи кожне обчислення відповідного значення та побічний ефект, визначається невизначено послідовно щодо будь-якого іншого параметра. [Примітка: Усі побічні ефекти обчислення аргументів послідовно розподіляються до введення функції (див. 4.6). —Кінець примітки] [Приклад:

void f() {
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, "");
assert(s == "I have heard it works only if you believe in it"); // OK
}

—Кінцевий приклад]


7
Я трохи здивований, побачивши, що "багато експертів" пропустили проблему. Загальновідомо, що оцінка виразу postfix виклику функції не проводиться послідовно, перш ніж оцінювати аргументи (у всіх версіях C і C ++).
MM

@ShafikYaghmour Виклики функції невизначено послідовно розташовані відносно один одного та всього іншого, за винятком згаданих раніше взаємозв'язків. Однак оцінки 1, 2, 3, 5, 6, 8, 9 "even", "don't"та декількох випадків не sє послідовними відносно один одного.
TC

4
@TC ні, це не так (саме так виникає ця "помилка"). Наприклад foo().func( bar() ), він може дзвонити foo()до або після дзвінка bar(). Постфікс-вираз є foo().func. Аргументи та постфікс-вираз послідовно розміщуються перед тілом func(), але без послідовності відносно один одного.
MM

@MattMcNabb Ах, так, я неправильно прочитав. Ви говорите про сам вираз postfix, а не про дзвінок. Так, це правильно, вони не виконуються (якщо, звичайно, не застосовується якесь інше правило).
TC

6
Існує також фактор того, що хтось схильний вважати, що код, що з’являється в книзі B.Stroustrup, є правильним, інакше хтось напевно вже помітив би! (пов’язано; користувачі SO все ще знаходять нові помилки в K&R)
MM

4

Це призначено для додавання інформації з цього питання стосовно C ++ 17. Пропозиція ( Уточнення Порядку оцінки виразів для Ідіоматичної версії C ++ 2 ) щодо C++17вирішення проблеми із посиланням на наведений вище код була взірцем.

Як було запропоновано, я додав відповідну інформацію із пропозиції та цитую (підкреслює мою):

Порядок оцінки виразів, як це наразі зазначено у стандарті, підриває поради, популярні ідіоми програмування або відносну безпеку стандартних бібліотечних засобів. Пастки не лише для новачків або недбайливих програмістів. Вони впливають на всіх нас без розбору, навіть коли ми знаємо правила.

Розглянемо такий фрагмент програми:

void f()
{
  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, "");
  assert(s == "I have heard it works only if you believe in it");
}

Це твердження має підтвердити передбачуваний результат програміста. Він використовує "ланцюжок" викликів функцій-членів, що є звичайною стандартною практикою. Цей код був розглянутий експертами С ++ у всьому світі та опублікований (Мова програмування С ++, 4-е видання.) Однак його вразливість до невстановленого порядку оцінки була виявлена ​​лише нещодавно інструментом.

У статті пропонується змінити попереднє C++17правило щодо порядку оцінки висловлювань, яке зазнало впливу Cта існувало більше трьох десятиліть. Він пропонував, щоб мова повинна гарантувати сучасні ідіоми або ризикувати "пастками та джерелами незрозумілих, важко знайти помилок", такими як те, що сталося із зразком коду вище.

Пропозиція C++17має вимагати , щоб кожен вираз має чітко визначений порядок оцінки :

  • Вирази постфіксу обчислюються зліва направо. Це включає виклики функцій та вирази вибору членів.
  • Вирази призначення визначаються справа наліво. Сюди входять складні завдання.
  • Операнди для операторів зсуву обчислюються зліва направо.
  • Порядок обчислення виразу, що включає перевантажений оператор, визначається порядком, пов'язаним з відповідним вбудованим оператором, а не правилами викликів функцій.

Наведений вище код успішно компілюється за допомогою GCC 7.1.1та Clang 4.0.0.

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