Код демонстрирует неопределенное поведение из-за неопределенного порядка оценки подвыражений, хотя он не вызывает неопределенного поведения, поскольку все побочные эффекты выполняются внутри функций который вводит отношение последовательности между побочными эффектами в этом случае.
Этот пример упоминается в предложении N4228: Уточнение оценки выражений. Заказ на Idiomatic C++, в котором говорится следующее о коде в вопросе:
[...] Этот код был проверен экспертами по C++ со всего мира и опубликован (Язык программирования C++, 4th издание). Тем не менее, его уязвимость для неопределенного порядка оценки была обнаружена только недавно с помощью инструмента[...]
Подробнее
Для многих может быть очевидным, что аргументы функций имеют неопределенный порядок оценки, но, вероятно, не так очевидно, как это поведение взаимодействует с цепочками вызовов функций. Это было не очевидно для меня, когда я впервые проанализировал этот случай, и, очевидно, не для всех экспертов-рецензентов.
На первый взгляд может показаться, что, поскольку каждый 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
to 9
are indeterminately sequenced with respect to other sub-expressions with some of the exceptions listed below
1
to 3
are sequenced before 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
Выполнение программы, в котором говорится:
Если не указано иное, вычисления операндов отдельных операторов и подвыражений отдельных выражений не упорядочены.[...]
и мы знаем, что вызов функции вводит упорядоченное перед отношением постфиксного выражения и аргументов вызова функции по отношению к телу функции из раздела 1.9
:
[...] При вызове функции (независимо от того, является ли функция встроенной) каждое вычисление значения и побочный эффект, связанные с любым выражением аргумента или с постфиксным выражением, обозначающим вызываемую функцию, упорядочены перед выполнением каждого выражения или инструкции в теле вызываемой функции.[...]
Мы также знаем, что доступ к членам класса и, следовательно, цепочка будут оцениваться слева направо, начиная с раздела 5.2.5
Доступ к членам класса, в котором говорится:
[...] Оценивается постфиксное выражение перед точкой или стрелкой;64 результат этого вычисления вместе с id-выражением определяет результат всего постфиксного выражения.
Обратите внимание: в случае, когда выражение-id становится нестатической функцией-членом, порядок вычисления списка-выражений в пределах ()
не указывается, поскольку это отдельное подвыражение. Соответствующая грамматика из 5.2
постфиксных выражений:
postfix-expression:
postfix-expression ( expression-listopt) // function call
postfix-expression . templateopt id-expression // Class member access, ends
// up as a postfix-expression
С++ 17 изменения
Предложение p0145r3: уточнение порядка оценки выражений для идиоматических C++ внес несколько изменений. Включая изменения, которые обеспечивают правильное поведение кода, усиливая порядок правил оценки для постфиксных выражений и их списка выражений.
[expr.call]p5 говорит:
Постфиксное выражение располагается перед каждым выражением в списке выражений и любым аргументом по умолчанию. Инициализация параметра, включая каждое вычисление связанного значения и побочный эффект, неопределенно упорядочена по отношению к любому другому параметру. [Примечание: все побочные эффекты оценки аргументов упорядочиваются до входа в функцию (см. 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
}
— конец примера]
person
Shafik Yaghmour
schedule
26.11.2014
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );
- person Ben Voigt   schedule 27.11.2014cout << a << b << c
≡operator<<(operator<<(operator<<(cout, a), b), c)
лишь незначительно менее уродливо. - person Oktalist   schedule 01.12.2014