Код демонстрирует неопределенное поведение из-за неопределенного порядка оценки подвыражений, хотя он не вызывает неопределенное поведение, поскольку все побочные эффекты выполняются внутри функций, что в данном случае вводит взаимосвязь между побочными эффектами.
Этот пример упоминается в предложении N4228: Уточнение порядка оценки выражений для идиоматического C ++, в котором говорится следующее о коде в вопросе:
[...] Этот код был проверен экспертами C ++ по всему миру и опубликован (The C ++ Programming Language, 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
Выполнение программы», в котором говорится:
Если не указано иное, вычисления операндов отдельных операторов и подвыражений отдельных выражений неупорядочены. [...]
и мы знаем, что вызов функции вводит последовательную связь перед вызовом функции постфиксным выражением и аргументами по отношению к телу функции из раздела 1.9
:
[...] При вызове функции (независимо от того, является ли функция встроенной), каждое вычисление значения и побочный эффект, связанный с любым выражением аргумента или с постфиксным выражением, обозначающим вызываемую функцию, упорядочиваются перед выполнением каждого выражения или оператора в теле вызываемой функции. [...]
Мы также знаем, что доступ к членам класса и, следовательно, цепочка будут оцениваться слева направо из раздела 5.2.5
Доступ к членам класса, в котором говорится:
[...] Постфиксное выражение перед точкой или стрелкой оценивается; 64
результат этой оценки вместе с id-выражением определяет результат всего постфиксного выражения.
Обратите внимание, что в случае, когда 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: Уточнение порядка оценки выражений для идиоматического 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
}
—Конечный пример]
s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );