Код демонструє невизначену поведінку через невизначений порядок оцінки підвиразів, хоча він не викликає невизначеної поведінки, оскільки всі побічні ефекти виконуються в межах функцій, що вводить взаємозв'язок послідовності між побічними ефектами в цьому випадку.
Цей приклад згадується у пропозиції 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
}
—Кінцевий приклад]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );