c ++ 11 — выполняет ли этот код «Язык программирования C ++»? Четвертое издание, раздел 36.3.6, имеет четко определенное поведение?

В Бьярне Страуструпе Язык программирования C ++ 4-е издание раздела 36.3.6 STL-подобные операции следующий код используется в качестве примера формирование цепочки:

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 (увидеть это в прямом эфире), но это не дает сбоя при использовании лязг (увидеть это в прямом эфире).

Почему я получаю разные результаты? Являются ли какие-либо из этих компиляторов неправильно вычисляющими выражение цепочки, или этот код демонстрирует некоторую форму неопределенные или же неопределенное поведение?

94

Решение

Код демонстрирует неопределенное поведение из-за неопределенного порядка вычисления подвыражений, хотя он не вызывает неопределенное поведение, поскольку все побочные эффекты выполняются внутри функций. который вводит отношения секвенирования между побочными эффектами в этом случае.

Этот пример упоминается в предложении 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-выражением,
определяет результат всего постфиксного выражения.

Обратите внимание, в случае, когда 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

C ++ 17 изменений

Предложение p0145r3: уточнение порядка оценки выражений для Idiomatic C ++ сделал несколько изменений. Включая изменения, которые дают коду хорошо определенное поведение, усиливая порядок правил оценки для Постфиксное-выражение и их список_выражений.

[Expr.call] р5 говорит:

Постфиксное выражение упорядочивается перед каждым выражением в списке выражений и любым аргументом по умолчанию..
инициализация параметра, включая каждое связанное вычисление значения и побочный эффект, является неопределенной
упорядочено по отношению к любому другому параметру. [Примечание: все побочные эффекты оценки аргументов
последовательность перед вводом функции (см. 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
}

— конец примера]

105

Другие решения

Это предназначено для добавления информации по этому вопросу в отношении C ++ 17. Предложение (Уточнение порядка оценки выражений для Idiomatic C ++ Revision 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");
}

Предполагается, что утверждение проверяет предполагаемый результат программиста. Он использует «цепочку» вызовов функций-членов, общепринятую стандартную практику. Этот код был рассмотрен экспертами C ++ по всему миру и опубликован (Язык программирования C ++, 4-е издание.) уязвимость к неопределенному порядку оценки был обнаружен только недавно инструментом.

В документе предлагается изменить предварительнуюC++17 правило о порядке выражения выражения, на которое повлияло C и существуют уже более трех десятилетий. Было предложено язык должен гарантировать современные идиомы или риск «ловушки и источники неясных, трудно найти ошибок» например, что случилось с примером кода выше.

Предложение для C++17 это к требует, чтобы каждое выражение имело четко определенный порядок оценки:

  • Постфиксные выражения оцениваются слева направо. Это включает вызовы функций и выражения выбора членов.
  • Выражения присваивания оцениваются справа налево. Это включает в себя составные задания.
  • Операнды для смещения операторов оцениваются слева направо.
  • Порядок вычисления выражения с участием перегруженного оператора определяется порядком, связанным с соответствующим встроенным оператором, а не правилами для вызовов функций.

Приведенный выше код успешно компилируется с использованием GCC 7.1.1 а также Clang 4.0.0,

4

По вопросам рекламы [email protected]