Имеет ли этот код из раздела 36.3.6 4-го издания языка программирования С++ четко определенное поведение?

В книге Бьярна Страуструпа Язык программирования 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 (посмотреть вживую), но при использовании Clang (посмотреть вживую).

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


person Shafik Yaghmour    schedule 26.11.2014    source источник
comment
Лучше: s.replace( s.replace( s.replace(0, 4, "" ).find( "even" ), 4, "only" ).find( " don't" ), 6, "" );   -  person Ben Voigt    schedule 27.11.2014
comment
кроме ошибок, я единственный, кто думает, что такой уродливый код не должен быть в книге?   -  person Karoly Horvath    schedule 27.11.2014
comment
@KarolyHorvath Обратите внимание, что cout << a << b << coperator<<(operator<<(operator<<(cout, a), b), c) лишь незначительно менее уродливо.   -  person Oktalist    schedule 01.12.2014
comment
@Oktalist: :) по крайней мере, я понял намерение. он одновременно обучает поиску имени в зависимости от аргумента и синтаксису оператора в кратком формате... и это не создает впечатления, что вы действительно должны писать такой код.   -  person Karoly Horvath    schedule 01.12.2014


Ответы (2)


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

Этот пример упоминается в предложении 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
comment
Я немного удивлен, увидев, что многие эксперты упустили из виду эту проблему, хорошо известно, что вычисление постфиксного выражения вызова функции не выполняется последовательно — перед вычислением аргументов (во всех версиях C и С++). - person M.M; 27.11.2014
comment
@ShafikYaghmour Вызовы функций имеют неопределенную последовательность по отношению друг к другу и ко всему остальному, за исключением отмеченных вами отношений в последовательности до. Однако оценка 1, 2, 3, 5, 6, 8, 9, "even", "don't" и несколько экземпляров s не упорядочены друг относительно друга. - person T.C.; 27.11.2014
comment
@TC нет, это не так (именно так возникает эта ошибка). Например. foo().func( bar() ) он может вызвать foo() до или после вызова bar(). постфиксное выражение равно foo().func . Аргументы и постфиксное выражение упорядочены перед телом func(), но не упорядочены друг относительно друга. - person M.M; 27.11.2014
comment
@MattMcNabb Ах, да, я неправильно понял. Вы говорите о самом postfix-expression, а не о вызове. Да, верно, они не упорядочены (если, конечно, не применяется какое-то другое правило). - person T.C.; 27.11.2014
comment
@MattMcNabb, жаль, что я не нашел этот код органически, он немного отличается, когда вы знаете, что есть проблема с кодом, по сравнению с тем, когда вы видите код, о котором у вас нет предвзятых представлений. Должен сказать, не было очевидно, где была конкретная проблема, хотя я знал, что искал, поэтому я понимаю, как это могло быть упущено. - person Shafik Yaghmour; 27.11.2014
comment
Есть также фактор, по которому код, появляющийся в книге Б. Страуструпа, склонен считать правильным, иначе кто-нибудь наверняка уже заметил бы! (связано; пользователи SO все еще находят новые ошибки в K&R) - person M.M; 27.11.2014

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

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

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

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

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

person ricky m    schedule 12.06.2017