Ответ 1
Код демонстрирует неопределенное поведение из-за неопределенного порядка вычисления подвыражений, хотя он не вызывает неопределенное поведение, поскольку все побочные эффекты выполняются в функциях, которые вводят последовательность последовательности между побочными эффектами в этом случае.
Этот пример упоминается в предложении N4228: уточнение порядка оценки выражений для Idiomatic C++, в котором говорится следующее о коде в вопросе:
[...] Этот код был рассмотрен экспертами C++ по всему миру и опубликован ([Язык программирования 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.
Обратите внимание, что в случае, когда id-выражение оказывается нестатической функцией-членом, оно не определяет порядок вычисления списка выражений в ()
поскольку это отдельное подвыражение. Соответствующая грамматика из 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 говорит:
Постфиксное выражение упорядочивается перед каждым выражением в списке выражений и любым аргументом по умолчанию. Инициализация параметра, включая каждое связанное с ним вычисление значения и побочный эффект, определяется неопределенным образом относительно последовательности любого другого параметра. [Примечание: все побочные эффекты при оценке аргументов секвенируются до входа в функцию (см. 4.6). —Конец примечания] [Пример:
void f() { std::string s = "but I have heard it works even if you dont believe in it"; s.replace(0, 4, "").replace(s.find("even"), 4, "only").replace(s.find(" dont"), 6, ""); assert(s == "I have heard it works only if you believe in it"); // OK }
- конец примера]